Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 59c3a0c47e87e8db699d25b757bb7b599215ddbd


Parents : 756e8a5
Author : Ivan <e46112d44649266d71fe2193e00a4710>
Signature : T66BB85Valid, signed by author
Date : 2026-07-20T15:43:36-05:00

feat: update RNS FileSync with directory listing and creation endpoints, improve UI components for folder management

Changes

36 files changed, 1838 insertions(+), 564 deletions(-)


Diff

diff --git a/meshchatx.rsm b/meshchatx.rsm
index 4106c4a9..49b08966 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ

diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 50a7105d..ec79217f 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -13880,6 +13880,49 @@ class ReticulumMeshChat:
return not_ready
return web.json_response({"files": self.rns_filesync_handler.list_files()})
+ @routes.get("/api/v1/filesync/directories")
+ async def filesync_directories(request):
+ not_ready = _filesync_require_handler()
+ if not_ready is not None:
+ return not_ready
+ path = request.rel_url.query.get("path")
+ try:
+ result = await asyncio.to_thread(
+ self.rns_filesync_handler.list_directories,
+ path,
+ )
+ except Exception as e:
+ return web.json_response({"message": str(e)}, status=500)
+ if not result.get("ok"):
+ return web.json_response(
+ {"message": result.get("error", "list directories failed")},
+ status=400,
+ )
+ return web.json_response(result)
+
+ @routes.post("/api/v1/filesync/directories")
+ async def filesync_directories_create(request):
+ not_ready = _filesync_require_handler()
+ if not_ready is not None:
+ return not_ready
+ data = await request.json()
+ if not isinstance(data, dict):
+ return web.json_response({"message": "Invalid JSON body"}, status=400)
+ try:
+ result = await asyncio.to_thread(
+ self.rns_filesync_handler.create_directory,
+ data.get("parent"),
+ data.get("name", ""),
+ )
+ except Exception as e:
+ return web.json_response({"message": str(e)}, status=500)
+ if not result.get("ok"):
+ return web.json_response(
+ {"message": result.get("error", "create directory failed")},
+ status=400,
+ )
+ return web.json_response(result)
+
@routes.post("/api/v1/filesync/connect")
async def filesync_connect(request):
not_ready = _filesync_require_handler()
@@ -22495,6 +22538,7 @@ class ReticulumMeshChat:
if not is_reaction_delivery and self.check_spam_keywords(
message_title,
message_content,
+ context=ctx,
):
is_spam = True
print(
@@ -22504,7 +22548,7 @@ class ReticulumMeshChat:
# reject attachments from blocked sources (already checked above, but double-check)
attachments_stripped = False
if has_attachments(lxmf_fields):
- if self.is_destination_blocked(source_hash):
+ if self.is_destination_blocked(source_hash, context=ctx):
print(
f"Rejecting LXMF message with attachments from blocked source: {source_hash}",
)

diff --git a/meshchatx/src/backend/plugin_manager.py b/meshchatx/src/backend/plugin_manager.py
index 490b0029..9a9db20f 100644
--- a/meshchatx/src/backend/plugin_manager.py
+++ b/meshchatx/src/backend/plugin_manager.py
@@ -1634,6 +1634,8 @@ class PluginManager:
existing = self._plugins.get(plugin_id)
if existing is None:
return True
+ if existing.tampered:
+ return True
if existing.version != manifest.get("version"):
return True
target_dir = os.path.join(self.installed_dir, plugin_id)

diff --git a/meshchatx/src/backend/plugin_signature.py b/meshchatx/src/backend/plugin_signature.py
index 32728b99..9811b521 100644
--- a/meshchatx/src/backend/plugin_signature.py
+++ b/meshchatx/src/backend/plugin_signature.py
@@ -34,8 +34,12 @@ def build_canonical_zip(entries: dict[str, bytes]) -> bytes:
def canonical_dir_payload(directory: str) -> bytes:
entries: dict[str, bytes] = {}
- for root, _dirs, files in os.walk(directory):
+ for root, dirs, files in os.walk(directory):
+ # Interpreter bytecode is created on import and must not affect integrity.
+ dirs[:] = [name for name in dirs if name != "__pycache__"]
for filename in files:
+ if filename.endswith((".pyc", ".pyo")):
+ continue
path = os.path.join(root, filename)
rel = os.path.relpath(path, directory).replace("\\", "/")
if is_signature_basename(rel):

diff --git a/meshchatx/src/backend/rns_filesync_handler.py b/meshchatx/src/backend/rns_filesync_handler.py
index fee597cb..e5d19793 100644
--- a/meshchatx/src/backend/rns_filesync_handler.py
+++ b/meshchatx/src/backend/rns_filesync_handler.py
@@ -197,6 +197,7 @@ class RnsFilesyncHandler:
status["monitor"] = self._monitor
status["announce_interval"] = self._announce_interval
status["config_directory"] = self._root
+ status["storage_directory"] = self.storage_dir
return status
return {
"running": False,
@@ -213,8 +214,98 @@ class RnsFilesyncHandler:
"monitor": self._monitor,
"announce_interval": self._announce_interval,
"config_directory": self._root,
+ "storage_directory": self.storage_dir,
}
+ def list_directories(self, path: str | None = None) -> dict[str, Any]:
+ """List subdirectories under identity storage for the folder browser."""
+ with self._lock:
+ root = self.storage_dir
+ cleaned = str(path or "").strip()
+ if cleaned:
+ target = self._resolve_sync_directory(cleaned)
+ if target is None:
+ return {
+ "ok": False,
+ "error": "path must stay under identity storage",
+ }
+ else:
+ os.makedirs(self._root, exist_ok=True)
+ target = self._root
+
+ if not os.path.isdir(target):
+ return {"ok": False, "error": "path is not a directory"}
+
+ entries: list[dict[str, str]] = []
+ try:
+ for name in sorted(os.listdir(target), key=str.lower):
+ if name.startswith("."):
+ continue
+ full = os.path.join(target, name)
+ if not os.path.isdir(full):
+ continue
+ resolved = os.path.realpath(full)
+ if resolved != root and not resolved.startswith(root + os.sep):
+ continue
+ entries.append({"name": name, "path": resolved})
+ except OSError as exc:
+ return {"ok": False, "error": str(exc)}
+
+ current = os.path.realpath(target)
+ parent = None
+ if current != root:
+ candidate = os.path.dirname(current)
+ if candidate == root or candidate.startswith(root + os.sep):
+ parent = candidate
+
+ return {
+ "ok": True,
+ "root": root,
+ "current": current,
+ "parent": parent,
+ "directories": entries,
+ }
+
+ def create_directory(self, parent: str | None, name: str) -> dict[str, Any]:
+ """Create a subdirectory under identity storage for sync folders."""
+ with self._lock:
+ cleaned_name = str(name or "").strip()
+ if (
+ not cleaned_name
+ or cleaned_name in (".", "..")
+ or "/" in cleaned_name
+ or "\\" in cleaned_name
+ or cleaned_name.startswith(".")
+ ):
+ return {"ok": False, "error": "invalid directory name"}
+
+ parent_cleaned = str(parent or "").strip()
+ if parent_cleaned:
+ parent_resolved = self._resolve_sync_directory(parent_cleaned)
+ else:
+ os.makedirs(self._root, exist_ok=True)
+ parent_resolved = self._root
+ if parent_resolved is None:
+ return {
+ "ok": False,
+ "error": "parent must stay under identity storage",
+ }
+
+ new_path = os.path.join(parent_resolved, cleaned_name)
+ resolved = self._resolve_sync_directory(new_path)
+ if resolved is None:
+ return {
+ "ok": False,
+ "error": "path must stay under identity storage",
+ }
+ try:
+ os.makedirs(resolved, exist_ok=False)
+ except FileExistsError:
+ return {"ok": False, "error": "directory already exists"}
+ except OSError as exc:
+ return {"ok": False, "error": str(exc)}
+ return {"ok": True, "path": resolved}
+
def start(
self,
*,

diff --git a/meshchatx/src/backend/rrc/hub_commands.py b/meshchatx/src/backend/rrc/hub_commands.py
index e3d070e9..1d4526fa 100644
--- a/meshchatx/src/backend/rrc/hub_commands.py
+++ b/meshchatx/src/backend/rrc/hub_commands.py
@@ -165,9 +165,30 @@ class HubCommandHandler:
except ValueError as e:
server._queue_notice(outgoing, link, None, "bad room: " + str(e))
return True
- st = server.rooms.ensure_state(r)
+ is_member = link in server._room_members.get(r, set())
+ is_server_op = server.policy.is_server_op(peer)
+ st = server.rooms.get_state(r)
+ is_room_op = False
+ if st is not None and isinstance(peer, (bytes, bytearray)):
+ founder = st.get("founder")
+ ops = st.get("ops") or set()
+ peer_b = bytes(peer)
+ if isinstance(founder, (bytes, bytearray)) and bytes(founder) == peer_b:
+ is_room_op = True
+ elif isinstance(ops, set) and peer_b in {bytes(x) for x in ops}:
+ is_room_op = True
+ can_see = is_member or is_room_op or is_server_op
if len(parts) == 2:
- topic = st.get("topic")
+ # Do not create ghost room state on read. Hide private topics from outsiders.
+ if st is not None and st.get("private") and not can_see:
+ server._queue_notice(
+ outgoing,
+ link,
+ room,
+ f"topic for {r}: (none)",
+ )
+ return True
+ topic = st.get("topic") if st is not None else None
server._queue_notice(
outgoing,
link,
@@ -175,6 +196,10 @@ class HubCommandHandler:
f"topic for {r}: {topic or '(none)'}",
)
return True
+ if not is_member and not is_server_op:
+ server._queue_error(outgoing, link, "not in room", room=r)
+ return True
+ st = server.rooms.ensure_state(r)
if not server.rooms.is_room_op(r, peer):
if bool(st.get("topic_ops_only")):
server._queue_error(outgoing, link, "not authorized (+t)", room=r)

diff --git a/meshchatx/src/backend/rrc/manager.py b/meshchatx/src/backend/rrc/manager.py
index 8ec76d64..bcf4982f 100644
--- a/meshchatx/src/backend/rrc/manager.py
+++ b/meshchatx/src/backend/rrc/manager.py
@@ -595,13 +595,14 @@ class RRCHub:
r = proto.normalize_room(room)
nick = self.get_effective_nick()
if record_local:
+ history_text = self._redact_command_for_history(text)
self._record_message(
proto.RRCMessage(
"msg",
r,
self.manager.identity.hash,
nick,
- text,
+ history_text,
proto.now_ms(),
),
local=True,
@@ -616,6 +617,18 @@ class RRCHub:
env[proto.K_NICK] = nick
self._send_env(env)
+ @staticmethod
+ def _redact_command_for_history(text):
+ """Omit +k secrets from locally recorded command history."""
+ parts = text.split()
+ if (
+ len(parts) >= 4
+ and parts[0].lower() == "/mode"
+ and parts[2].lower() == "+k"
+ ):
+ return " ".join([*parts[:3], "***"])
+ return text
+
def send_ping(self, room=None):
body = os.urandom(8)
env = proto.make_envelope(

diff --git a/meshchatx/src/frontend/components/ConfirmDialog.vue b/meshchatx/src/frontend/components/ConfirmDialog.vue
index abf79d2a..8237a3d7 100644
--- a/meshchatx/src/frontend/components/ConfirmDialog.vue
+++ b/meshchatx/src/frontend/components/ConfirmDialog.vue
@@ -69,6 +69,9 @@ export default {
},
methods: {
show({ message, resolve }) {
+ if (typeof this.resolvePromise === "function") {
+ this.resolvePromise(false);
+ }
this.pendingConfirm = { message };
this.resolvePromise = resolve;
},

diff --git a/meshchatx/src/frontend/components/PromptDialog.vue b/meshchatx/src/frontend/components/PromptDialog.vue
index f413fda5..dba2245f 100644
--- a/meshchatx/src/frontend/components/PromptDialog.vue
+++ b/meshchatx/src/frontend/components/PromptDialog.vue
@@ -83,6 +83,9 @@ export default {
},
methods: {
show({ message, defaultValue, resolve, inputType }) {
+ if (typeof this.resolvePromise === "function") {
+ this.resolvePromise(null);
+ }
this.pendingPrompt = { message };
this.inputValue = defaultValue == null ? "" : String(defaultValue);
this.inputType = inputType === "password" ? "password" : "text";

diff --git a/meshchatx/src/frontend/components/filesync/FilesyncDirectoryBrowserModal.vue b/meshchatx/src/frontend/components/filesync/FilesyncDirectoryBrowserModal.vue
new file mode 100644
index 00000000..725cc95b
--- /dev/null
+++ b/meshchatx/src/frontend/components/filesync/FilesyncDirectoryBrowserModal.vue
@@ -0,0 +1,213 @@
+<!-- SPDX-License-Identifier: 0BSD -->
+
+<template>
+ <div
+ v-if="open"
+ class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4"
+ @click.self="$emit('close')"
+ >
+ <div
+ class="flex w-full max-w-lg max-h-[min(36rem,90vh)] flex-col rounded-2xl border border-sem-border-card bg-sem-surface shadow-xl"
+ role="dialog"
+ :aria-label="$t('rns_filesync.browser_title')"
+ >
+ <div class="flex items-center justify-between gap-2 border-b border-sem-border px-5 py-4">
+ <h2 class="text-lg font-semibold text-sem-fg">{{ $t("rns_filesync.browser_title") }}</h2>
+ <button
+ type="button"
+ class="rounded-lg p-1 text-sem-fg-muted hover:bg-sem-surface-muted"
+ :title="$t('common.close')"
+ @click="$emit('close')"
+ >
+ <MaterialDesignIcon icon-name="close" class="size-5" />
+ </button>
+ </div>
+
+ <div class="px-5 pt-3 space-y-2">
+ <p class="text-xs text-sem-fg-muted">{{ $t("rns_filesync.browser_hint") }}</p>
+ <div class="flex items-center gap-2">
+ <button
+ type="button"
+ class="secondary-chip px-2.5 py-1.5 text-xs shrink-0"
+ :disabled="busy || !parent"
+ :title="$t('rns_filesync.browser_up')"
+ @click="goParent"
+ >
+ <MaterialDesignIcon icon-name="arrow-up" class="w-4 h-4" />
+ {{ $t("rns_filesync.browser_up") }}
+ </button>
+ <div class="input-field flex-1 min-w-0 !py-2 font-mono text-xs truncate" :title="current">
+ {{ current || "..." }}
+ </div>
+ </div>
+ </div>
+
+ <div class="flex-1 overflow-y-auto custom-scrollbar px-5 py-3">
+ <div v-if="busy && directories.length === 0" class="py-8 text-center text-sm text-sem-fg-muted">
+ {{ $t("rns_filesync.browser_loading") }}
+ </div>
+ <div v-else-if="directories.length === 0" class="py-8 text-center text-sm text-sem-fg-muted">
+ {{ $t("rns_filesync.browser_empty") }}
+ </div>
+ <ul v-else class="space-y-1">
+ <li v-for="entry in directories" :key="entry.path">
+ <button
+ type="button"
+ class="flex w-full items-center gap-2 rounded-lg border border-sem-border px-3 py-2 text-left text-sm transition-colors hover:bg-sem-surface-muted"
+ @click="enterDirectory(entry.path)"
+ >
+ <MaterialDesignIcon
+ icon-name="folder"
+ class="w-5 h-5 shrink-0 text-emerald-600 dark:text-emerald-400"
+ />
+ <span class="min-w-0 truncate">{{ entry.name }}</span>
+ </button>
+ </li>
+ </ul>
+ </div>
+
+ <div class="border-t border-sem-border px-5 py-3 space-y-3">
+ <div class="flex flex-col sm:flex-row gap-2">
+ <input
+ v-model="newFolderName"
+ type="text"
+ class="input-field flex-1 min-w-0 !py-2 text-sm"
+ :placeholder="$t('rns_filesync.browser_new_placeholder')"
+ :disabled="busy"
+ @keydown.enter.prevent="createFolder"
+ />
+ <button
+ type="button"
+ class="secondary-chip px-3 py-2 text-xs shrink-0"
+ :disabled="busy || !newFolderName.trim()"
+ @click="createFolder"
+ >
+ <MaterialDesignIcon icon-name="folder-plus-outline" class="w-4 h-4" />
+ {{ $t("rns_filesync.browser_new") }}
+ </button>
+ </div>
+ <div class="flex justify-end gap-2">
+ <button type="button" class="secondary-chip px-4 py-2 text-sm" @click="$emit('close')">
+ {{ $t("common.cancel") }}
+ </button>
+ <button
+ type="button"
+ class="primary-chip px-4 py-2 text-sm"
+ :disabled="busy || !current"
+ @click="confirmSelection"
+ >
+ {{ $t("rns_filesync.browser_select") }}
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+import MaterialDesignIcon from "../MaterialDesignIcon.vue";
+import ToastUtils from "../../js/ToastUtils";
+
+export default {
+ name: "FilesyncDirectoryBrowserModal",
+ components: {
+ MaterialDesignIcon,
+ },
+ props: {
+ open: {
+ type: Boolean,
+ default: false,
+ },
+ initialPath: {
+ type: String,
+ default: "",
+ },
+ },
+ emits: ["close", "select"],
+ data() {
+ return {
+ busy: false,
+ root: "",
+ current: "",
+ parent: null,
+ directories: [],
+ newFolderName: "",
+ };
+ },
+ watch: {
+ open: {
+ immediate: true,
+ async handler(isOpen) {
+ if (!isOpen) {
+ return;
+ }
+ this.newFolderName = "";
+ const start = String(this.initialPath || "").trim();
+ await this.load(start || undefined);
+ },
+ },
+ },
+ methods: {
+ async load(path) {
+ this.busy = true;
+ try {
+ const url = path
+ ? `/api/v1/filesync/directories?path=${encodeURIComponent(path)}`
+ : "/api/v1/filesync/directories";
+ const response = await window.api.get(url);
+ const data = response?.data || {};
+ this.root = data.root || "";
+ this.current = data.current || "";
+ this.parent = data.parent || null;
+ this.directories = Array.isArray(data.directories) ? data.directories : [];
+ } catch (err) {
+ ToastUtils.error(err?.message || this.$t("rns_filesync.error"));
+ } finally {
+ this.busy = false;
+ }
+ },
+ async enterDirectory(path) {
+ await this.load(path);
+ },
+ async goParent() {
+ if (!this.parent) {
+ return;
+ }
+ await this.load(this.parent);
+ },
+ async createFolder() {
+ const name = String(this.newFolderName || "").trim();
+ if (!name) {
+ return;
+ }
+ this.busy = true;
+ try {
+ const response = await window.api.post("/api/v1/filesync/directories", {
+ parent: this.current,
+ name,
+ });
+ const created = response?.data?.path;
+ this.newFolderName = "";
+ ToastUtils.success(this.$t("rns_filesync.browser_created"));
+ if (created) {
+ await this.load(created);
+ } else {
+ await this.load(this.current);
+ }
+ } catch (err) {
+ ToastUtils.error(err?.message || this.$t("rns_filesync.error"));
+ } finally {
+ this.busy = false;
+ }
+ },
+ confirmSelection() {
+ const path = String(this.current || "").trim();
+ if (!path) {
+ return;
+ }
+ this.$emit("select", path);
+ this.$emit("close");
+ },
+ },
+};
+</script>

diff --git a/meshchatx/src/frontend/components/filesync/RnsFilesyncPage.vue b/meshchatx/src/frontend/components/filesync/RnsFilesyncPage.vue
index 6a4b7c3b..3d5a4677 100644
--- a/meshchatx/src/frontend/components/filesync/RnsFilesyncPage.vue
+++ b/meshchatx/src/frontend/components/filesync/RnsFilesyncPage.vue
@@ -30,7 +30,7 @@
</div>
<div
- class="border-b border-gray-200 dark:border-zinc-700 overflow-x-auto overscroll-x-contain -mx-4 px-4 sm:mx-0 sm:px-0"
+ class="border-b border-sem-border overflow-x-auto overscroll-x-contain -mx-4 px-4 sm:mx-0 sm:px-0"
>
<div class="flex w-max min-w-full sm:w-auto gap-1 sm:gap-2">
<button
@@ -40,7 +40,7 @@
:class="[
activeTab === tab.id
? 'border-b-2 border-emerald-500 text-emerald-600 dark:text-emerald-400'
- : 'text-gray-600 dark:text-gray-400',
+ : 'text-sem-fg-muted',
'shrink-0 px-3 sm:px-4 py-2 text-sm font-semibold transition',
]"
@click="activeTab = tab.id"
@@ -50,111 +50,165 @@
</div>
</div>
- <div v-if="activeTab === 'status'" class="space-y-4">
- <div class="grid gap-3 sm:grid-cols-2">
- <div>
- <label class="glass-label">{{ $t("rns_filesync.sync_directory") }}</label>
- <input
- v-model="syncDirectory"
- type="text"
- class="glass-input w-full font-mono text-sm"
- :disabled="status.running"
- :placeholder="$t('rns_filesync.sync_directory_placeholder')"
- />
- </div>
- <div>
- <label class="glass-label">{{ $t("rns_filesync.announce_interval") }}</label>
- <input
- v-model.number="announceInterval"
- type="number"
- min="10"
- class="glass-input w-full"
- />
- </div>
- </div>
- <label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
- <input v-model="monitor" type="checkbox" class="rounded" />
- {{ $t("rns_filesync.monitor") }}
- </label>
- <div class="flex flex-wrap gap-2">
- <button
- v-if="!status.running"
- type="button"
- class="primary-chip px-4 py-2 text-sm"
- :disabled="busy"
- @click="startService"
- >
- <MaterialDesignIcon icon-name="play" class="w-4 h-4" />
- {{ $t("rns_filesync.start") }}
- </button>
- <button
- v-else
- type="button"
- class="secondary-chip px-4 py-2 text-sm text-red-600 dark:text-red-300 border-red-200 dark:border-red-500/50"
- :disabled="busy"
- @click="stopService"
- >
- <MaterialDesignIcon icon-name="stop" class="w-4 h-4" />
- {{ $t("rns_filesync.stop") }}
- </button>
- <button
- type="button"
- class="secondary-chip px-4 py-2 text-sm"
- :disabled="busy || !status.running"
- @click="announceNow"
- >
- <MaterialDesignIcon icon-name="bullhorn" class="w-4 h-4" />
- {{ $t("rns_filesync.announce") }}
- </button>
- <button
- type="button"
- class="secondary-chip px-4 py-2 text-sm"
- :disabled="busy"
- @click="refreshStatus"
- >
- <MaterialDesignIcon icon-name="refresh" class="w-4 h-4" />
- {{ $t("rns_filesync.refresh") }}
- </button>
- </div>
- <div class="space-y-2 text-sm">
- <div class="flex flex-wrap gap-x-4 gap-y-1">
- <span>
- {{ $t("rns_filesync.running") }}:
- <strong>{{
- status.running ? $t("rns_filesync.yes") : $t("rns_filesync.no")
- }}</strong>
+ <div v-if="activeTab === 'folder'" class="space-y-4">
+ <div class="rounded-xl border border-sem-border bg-sem-surface-muted/40 p-4 space-y-4">
+ <div class="flex flex-wrap items-center gap-2">
+ <span
+ class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-semibold"
+ :class="
+ status.running
+ ? 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300'
+ : 'bg-sem-surface-muted text-sem-fg-muted'
+ "
+ >
+ <span
+ class="size-1.5 rounded-full"
+ :class="status.running ? 'bg-emerald-500' : 'bg-sem-fg-muted'"
+ ></span>
+ {{
+ status.running
+ ? $t("rns_filesync.status_syncing")
+ : $t("rns_filesync.status_stopped")
+ }}
</span>
- <span>
+ <span class="text-xs text-sem-fg-muted">
{{ $t("rns_filesync.peers_count") }}:
- <strong>{{ status.peers || 0 }}</strong>
+ <strong class="text-sem-fg">{{ status.peers || 0 }}</strong>
</span>
- <span>
+ <span class="text-xs text-sem-fg-muted">
{{ $t("rns_filesync.files_count") }}:
- <strong>{{ status.files || 0 }}</strong>
+ <strong class="text-sem-fg">{{ status.files || 0 }}</strong>
</span>
</div>
- <div v-if="status.destination_hash" class="font-mono text-xs break-all">
- <div class="font-semibold mb-1">{{ $t("rns_filesync.destination_hash") }}</div>
+
+ <div>
+ <label class="glass-label">{{ $t("rns_filesync.sync_directory") }}</label>
+ <div class="flex gap-2">
+ <input
+ v-model="syncDirectory"
+ type="text"
+ class="input-field flex-1 min-w-0 font-mono text-sm"
+ :disabled="status.running"
+ :placeholder="$t('rns_filesync.sync_directory_placeholder')"
+ />
+ <button
+ type="button"
+ class="secondary-chip px-3 py-2 text-xs shrink-0"
+ :disabled="busy || status.running"
+ :title="$t('rns_filesync.browse_folder')"
+ @click="openDirectoryBrowser"
+ >
+ <MaterialDesignIcon icon-name="folder-open-outline" class="w-4 h-4" />
+ <span class="hidden sm:inline">{{ $t("rns_filesync.browse_folder") }}</span>
+ </button>
+ <button
+ type="button"
+ class="secondary-chip px-3 py-2 text-xs shrink-0"
+ :disabled="busy || !syncDirectory"
+ :title="$t('rns_filesync.open_folder')"
+ @click="openSyncFolder"
+ >
+ <MaterialDesignIcon icon-name="folder" class="w-4 h-4" />
+ <span class="hidden sm:inline">{{ $t("rns_filesync.open_folder") }}</span>
+ </button>
+ </div>
+ <p class="mt-1.5 text-xs text-sem-fg-muted">
+ {{ $t("rns_filesync.sync_directory_help") }}
+ </p>
+ </div>
+
+ <div class="grid gap-3 sm:grid-cols-2">
+ <div>
+ <label class="glass-label">{{ $t("rns_filesync.announce_interval") }}</label>
+ <input
+ v-model.number="announceInterval"
+ type="number"
+ min="10"
+ class="input-field w-full"
+ />
+ <p class="mt-1 text-xs text-sem-fg-muted">
+ {{ $t("rns_filesync.announce_interval_help") }}
+ </p>
+ </div>
+ <div class="flex items-end">
+ <label class="flex items-center gap-2 text-sm text-sem-fg pb-2">
+ <input v-model="monitor" type="checkbox" class="rounded" />
+ {{ $t("rns_filesync.monitor") }}
+ </label>
+ </div>
+ </div>
+
+ <div class="flex flex-wrap gap-2">
+ <button
+ v-if="!status.running"
+ type="button"
+ class="primary-chip px-4 py-2 text-sm"
+ :disabled="busy"
+ @click="startService"
+ >
+ <MaterialDesignIcon icon-name="play" class="w-4 h-4" />
+ {{ $t("rns_filesync.start") }}
+ </button>
<button
+ v-else
type="button"
- class="text-left hover:underline"
- @click="copyHash(status.destination_hash)"
+ class="secondary-chip px-4 py-2 text-sm text-red-600 dark:text-red-300 border-red-200 dark:border-red-500/50"
+ :disabled="busy"
+ @click="stopService"
+ >
+ <MaterialDesignIcon icon-name="stop" class="w-4 h-4" />
+ {{ $t("rns_filesync.stop") }}
+ </button>
+ <button
+ type="button"
+ class="secondary-chip px-4 py-2 text-sm"
+ :disabled="busy || !status.running"
+ @click="announceNow"
>
- {{ status.destination_hash }}
+ <MaterialDesignIcon icon-name="bullhorn" class="w-4 h-4" />
+ {{ $t("rns_filesync.announce") }}
+ </button>
+ <button
+ type="button"
+ class="secondary-chip px-4 py-2 text-sm"
+ :disabled="busy"
+ @click="refreshStatus"
+ >
+ <MaterialDesignIcon icon-name="refresh" class="w-4 h-4" />
+ {{ $t("rns_filesync.refresh") }}
</button>
</div>
- <div v-if="lastProgress" class="text-xs text-gray-600 dark:text-gray-400">
- {{ $t("rns_filesync.last_progress") }}: {{ lastProgress }}
+ </div>
+
+ <div v-if="status.destination_hash" class="rounded-xl border border-sem-border p-4 space-y-2">
+ <div class="text-sm font-semibold text-sem-fg">
+ {{ $t("rns_filesync.share_id") }}
</div>
+ <p class="text-xs text-sem-fg-muted">{{ $t("rns_filesync.share_id_help") }}</p>
+ <button
+ type="button"
+ class="w-full text-left font-mono text-xs break-all rounded-lg border border-sem-border bg-sem-surface-muted/50 px-3 py-2 hover:border-emerald-500"
+ @click="copyHash(status.destination_hash)"
+ >
+ {{ status.destination_hash }}
+ </button>
+ </div>
+
+ <div
+ v-if="lastProgressLabel"
+ class="rounded-lg border border-sem-border px-3 py-2 text-xs text-sem-fg-muted"
+ >
+ {{ $t("rns_filesync.last_progress") }}: {{ lastProgressLabel }}
</div>
</div>
- <div v-else-if="activeTab === 'peers'" class="space-y-4">
+ <div v-else-if="activeTab === 'devices'" class="space-y-4">
+ <p class="text-sm text-sem-fg-muted">{{ $t("rns_filesync.devices_help") }}</p>
<div class="flex flex-col sm:flex-row gap-2">
<input
v-model="connectHash"
type="text"
- class="glass-input flex-1 font-mono text-sm"
+ class="input-field flex-1 font-mono text-sm"
:placeholder="$t('rns_filesync.peer_hash_placeholder')"
/>
<button
@@ -163,6 +217,7 @@
:disabled="busy || !status.running"
@click="connectPeer"
>
+ <MaterialDesignIcon icon-name="link-variant" class="w-4 h-4" />
{{ $t("rns_filesync.connect") }}
</button>
</div>
@@ -174,19 +229,22 @@
>
{{ $t("rns_filesync.refresh") }}
</button>
- <div v-if="peers.length === 0" class="text-sm text-gray-500 dark:text-gray-400">
+ <div v-if="peers.length === 0" class="text-sm text-sem-fg-muted">
{{ $t("rns_filesync.no_peers") }}
</div>
<ul v-else class="space-y-2">
<li
v-for="peer in peers"
:key="peer.peer_id"
- class="flex flex-col sm:flex-row sm:items-center justify-between gap-2 p-3 rounded-lg border border-gray-200 dark:border-zinc-700"
+ class="flex flex-col sm:flex-row sm:items-center justify-between gap-2 p-3 rounded-lg border border-sem-border"
>
- <div class="min-w-0 font-mono text-xs break-all">
- <div>{{ peer.peer_id }}</div>
- <div class="text-gray-500 dark:text-gray-400">
- {{ peer.destination_hash }} · {{ peer.status }}
+ <div class="min-w-0">
+ <div class="font-mono text-xs break-all text-sem-fg">{{ peer.peer_id }}</div>
+ <div class="text-xs text-sem-fg-muted mt-1">
+ {{ peerStatusLabel(peer) }}
+ <span v-if="peer.destination_hash" class="font-mono">
+ · {{ peer.destination_hash }}
+ </span>
</div>
</div>
<button
@@ -202,34 +260,43 @@
</div>
<div v-else-if="activeTab === 'files'" class="space-y-4">
- <button
- type="button"
- class="secondary-chip px-3 py-1.5 text-sm"
- :disabled="busy"
- @click="refreshFiles"
- >
- {{ $t("rns_filesync.refresh") }}
- </button>
- <div v-if="files.length === 0" class="text-sm text-gray-500 dark:text-gray-400">
+ <div class="flex flex-wrap items-center gap-2">
+ <button
+ type="button"
+ class="secondary-chip px-3 py-1.5 text-sm"
+ :disabled="busy"
+ @click="refreshFiles"
+ >
+ {{ $t("rns_filesync.refresh") }}
+ </button>
+ <button
+ type="button"
+ class="secondary-chip px-3 py-1.5 text-sm"
+ :disabled="busy || !syncDirectory"
+ @click="openSyncFolder"
+ >
+ <MaterialDesignIcon icon-name="folder-open-outline" class="w-4 h-4" />
+ {{ $t("rns_filesync.open_folder") }}
+ </button>
+ </div>
+ <div v-if="files.length === 0" class="text-sm text-sem-fg-muted">
{{ $t("rns_filesync.no_files") }}
</div>
<ul v-else class="space-y-2">
- <li
- v-for="file in files"
- :key="file.path"
- class="p-3 rounded-lg border border-gray-200 dark:border-zinc-700 font-mono text-xs"
- >
- <div class="break-all">{{ file.path }}</div>
- <div class="text-gray-500 dark:text-gray-400 mt-1">
- {{ file.size }} B · {{ file.hash }}
+ <li v-for="file in files" :key="file.path" class="p-3 rounded-lg border border-sem-border">
+ <div class="break-all text-sm text-sem-fg">{{ file.path }}</div>
+ <div class="text-xs text-sem-fg-muted mt-1">
+ {{ formatFileSize(file.size) }}
+ <span v-if="file.hash" class="font-mono"> · {{ shortHash(file.hash) }}</span>
</div>
</li>
</ul>
</div>
- <div v-else-if="activeTab === 'browse'" class="space-y-4">
+ <div v-else-if="activeTab === 'remote'" class="space-y-4">
+ <p class="text-sm text-sem-fg-muted">{{ $t("rns_filesync.remote_help") }}</p>
<div class="flex flex-col sm:flex-row gap-2">
- <select v-model="browsePeerId" class="glass-input flex-1 font-mono text-sm">
+ <select v-model="browsePeerId" class="input-field flex-1 font-mono text-sm">
<option value="">{{ $t("rns_filesync.select_peer") }}</option>
<option v-for="peer in peers" :key="peer.peer_id" :value="peer.peer_id">
{{ peer.peer_id }}
@@ -241,21 +308,24 @@
:disabled="busy || !status.running || !browsePeerId"
@click="browsePeer"
>
+ <MaterialDesignIcon icon-name="folder-open-outline" class="w-4 h-4" />
{{ $t("rns_filesync.browse") }}
</button>
</div>
- <div v-if="remoteFiles.length === 0" class="text-sm text-gray-500 dark:text-gray-400">
+ <div v-if="remoteFiles.length === 0" class="text-sm text-sem-fg-muted">
{{ $t("rns_filesync.no_remote_files") }}
</div>
<ul v-else class="space-y-2">
<li
v-for="file in remoteFiles"
:key="file.path || file"
- class="flex flex-col sm:flex-row sm:items-center justify-between gap-2 p-3 rounded-lg border border-gray-200 dark:border-zinc-700"
+ class="flex flex-col sm:flex-row sm:items-center justify-between gap-2 p-3 rounded-lg border border-sem-border"
>
- <div class="min-w-0 font-mono text-xs break-all">
+ <div class="min-w-0 text-sm break-all text-sem-fg">
{{ file.path || file }}
- <span v-if="file.size != null" class="text-gray-500"> · {{ file.size }} B</span>
+ <span v-if="file.size != null" class="text-xs text-sem-fg-muted">
+ · {{ formatFileSize(file.size) }}
+ </span>
</div>
<button
type="button"
@@ -269,48 +339,66 @@
</ul>
</div>
- <div v-else-if="activeTab === 'acl'" class="space-y-4">
- <label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
+ <div v-else-if="activeTab === 'sharing'" class="space-y-4">
+ <p class="text-sm text-sem-fg-muted">{{ $t("rns_filesync.sharing_help") }}</p>
+ <label class="flex items-center gap-2 text-sm text-sem-fg">
<input v-model="aclEnforce" type="checkbox" class="rounded" @change="saveEnforce" />
{{ $t("rns_filesync.acl_enforce") }}
</label>
- <div class="flex flex-col sm:flex-row gap-2">
+ <div class="flex flex-col gap-3">
<input
v-model="aclHash"
type="text"
- class="glass-input flex-1 font-mono text-sm"
+ class="input-field w-full font-mono text-sm"
:placeholder="$t('rns_filesync.peer_hash_placeholder')"
/>
- <div class="flex items-center gap-3 text-sm">
- <label class="flex items-center gap-1">
- <input v-model="aclRead" type="checkbox" />
- r
+ <div class="flex flex-wrap items-center gap-4 text-sm text-sem-fg">
+ <label class="flex items-center gap-1.5">
+ <input v-model="aclRead" type="checkbox" class="rounded" />
+ {{ $t("rns_filesync.perm_read") }}
</label>
- <label class="flex items-center gap-1">
- <input v-model="aclWrite" type="checkbox" />
- w
+ <label class="flex items-center gap-1.5">
+ <input v-model="aclWrite" type="checkbox" class="rounded" />
+ {{ $t("rns_filesync.perm_write") }}
</label>
- <label class="flex items-center gap-1">
- <input v-model="aclDelete" type="checkbox" />
- d
+ <label class="flex items-center gap-1.5">
+ <input v-model="aclDelete" type="checkbox" class="rounded" />
+ {{ $t("rns_filesync.perm_delete") }}
</label>
</div>
<button
type="button"
- class="primary-chip px-4 py-2 text-sm"
+ class="primary-chip px-4 py-2 text-sm self-start"
:disabled="busy"
@click="grantAcl"
>
{{ $t("rns_filesync.acl_grant") }}
</button>
</div>
- <pre class="p-3 rounded-lg bg-zinc-100 dark:bg-zinc-900 text-xs overflow-x-auto">{{
- aclRulesText
- }}</pre>
+ <div v-if="aclRows.length === 0" class="text-sm text-sem-fg-muted">
+ {{ $t("rns_filesync.no_acl_rules") }}
+ </div>
+ <ul v-else class="space-y-2">
+ <li
+ v-for="row in aclRows"
+ :key="row.hash"
+ class="p-3 rounded-lg border border-sem-border text-sm"
+ >
+ <div class="font-mono text-xs break-all text-sem-fg">{{ row.hash }}</div>
+ <div class="text-xs text-sem-fg-muted mt-1">{{ row.permsLabel }}</div>
+ </li>
+ </ul>
</div>
</div>
</div>
</div>
+
+ <FilesyncDirectoryBrowserModal
+ :open="directoryBrowserOpen"
+ :initial-path="syncDirectory"
+ @close="directoryBrowserOpen = false"
+ @select="onDirectorySelected"
+ />
</div>
</template>
@@ -318,6 +406,9 @@
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import ToastUtils from "../../js/ToastUtils";
import ToolsPageHeader from "../tools/ToolsPageHeader.vue";
+import FilesyncDirectoryBrowserModal from "./FilesyncDirectoryBrowserModal.vue";
+import ElectronUtils from "../../js/ElectronUtils";
+import Utils from "../../js/Utils";
import { onWsEvent, offWsEvent } from "../../js/registries/wsEventRegistry.js";
export default {
@@ -325,16 +416,17 @@ export default {
components: {
MaterialDesignIcon,
ToolsPageHeader,
+ FilesyncDirectoryBrowserModal,
},
data() {
return {
- activeTab: "status",
+ activeTab: "folder",
tabs: [
- { id: "status", labelKey: "rns_filesync.tab_status" },
- { id: "peers", labelKey: "rns_filesync.tab_peers" },
+ { id: "folder", labelKey: "rns_filesync.tab_folder" },
+ { id: "devices", labelKey: "rns_filesync.tab_devices" },
{ id: "files", labelKey: "rns_filesync.tab_files" },
- { id: "browse", labelKey: "rns_filesync.tab_browse" },
- { id: "acl", labelKey: "rns_filesync.tab_acl" },
+ { id: "remote", labelKey: "rns_filesync.tab_remote" },
+ { id: "sharing", labelKey: "rns_filesync.tab_sharing" },
],
busy: false,
status: {
@@ -343,6 +435,7 @@ export default {
files: 0,
destination_hash: null,
sync_directory: "",
+ storage_directory: "",
},
syncDirectory: "",
announceInterval: 300,
@@ -358,17 +451,71 @@ export default {
aclWrite: false,
aclDelete: false,
aclRules: {},
- lastProgress: "",
+ lastProgress: null,
+ directoryBrowserOpen: false,
wsHandlers: [],
};
},
computed: {
- aclRulesText() {
- try {
- return JSON.stringify(this.aclRules || {}, null, 2);
- } catch {
- return "{}";
+ lastProgressLabel() {
+ const payload = this.lastProgress;
+ if (!payload) {
+ return "";
+ }
+ if (typeof payload === "string") {
+ return payload;
+ }
+ const path = payload.path || payload.file || payload.name || "";
+ const status = payload.status || payload.state || payload.phase || "";
+ const bytes = payload.bytes ?? payload.transferred ?? payload.done;
+ const total = payload.total ?? payload.size;
+ const parts = [];
+ if (path) {
+ parts.push(String(path));
+ }
+ if (status) {
+ parts.push(String(status));
+ }
+ if (bytes != null && total != null) {
+ parts.push(`${this.formatFileSize(bytes)} / ${this.formatFileSize(total)}`);
+ } else if (bytes != null) {
+ parts.push(this.formatFileSize(bytes));
+ }
+ if (parts.length === 0) {
+ try {
+ return JSON.stringify(payload);
+ } catch {
+ return String(payload);
+ }
+ }
+ return parts.join(" · ");
+ },
+ aclRows() {
+ const rules = this.aclRules || {};
+ const byHash = {};
+ for (const perm of ["read", "write", "delete"]) {
+ const targets = Array.isArray(rules[perm]) ? rules[perm] : [];
+ for (const hash of targets) {
+ if (!byHash[hash]) {
+ byHash[hash] = new Set();
+ }
+ byHash[hash].add(perm);
+ }
}
+ const labelMap = {
+ read: this.$t("rns_filesync.perm_read"),
+ write: this.$t("rns_filesync.perm_write"),
+ delete: this.$t("rns_filesync.perm_delete"),
+ };
+ return Object.keys(byHash)
+ .sort()
+ .map((hash) => ({
+ hash,
+ permsLabel: ["read", "write", "delete"]
+ .filter((p) => byHash[hash].has(p))
+ .map((p) => labelMap[p])
+ .join(", "),
+ }));
},
},
async mounted() {
@@ -388,7 +535,7 @@ export default {
this.wsHandlers.push([type, handler]);
};
bind("filesync.sync.progress", (payload) => {
- this.lastProgress = JSON.stringify(payload);
+ this.lastProgress = payload && typeof payload === "object" ? payload : { status: String(payload) };
});
bind("filesync.peer.connected", async () => {
await this.refreshPeers();
@@ -411,6 +558,53 @@ export default {
ToastUtils.error(`${this.$t("rns_filesync.error")}${detail ? ": " + detail : ""}`);
});
},
+ formatFileSize(bytes) {
+ return Utils.formatBytes(bytes || 0);
+ },
+ shortHash(hash) {
+ const value = String(hash || "");
+ if (value.length <= 12) {
+ return value;
+ }
+ return `${value.slice(0, 8)}...`;
+ },
+ peerStatusLabel(peer) {
+ const raw = peer?.status;
+ if (raw === 1 || raw === "connected" || raw === true) {
+ return this.$t("rns_filesync.peer_connected");
+ }
+ if (raw === 0 || raw === "disconnected" || raw === false) {
+ return this.$t("rns_filesync.peer_disconnected");
+ }
+ return raw != null ? String(raw) : this.$t("rns_filesync.peer_unknown");
+ },
+ openDirectoryBrowser() {
+ if (this.status.running) {
+ ToastUtils.warning(this.$t("rns_filesync.stop_before_change_folder"));
+ return;
+ }
+ this.directoryBrowserOpen = true;
+ },
+ onDirectorySelected(path) {
+ const cleaned = String(path || "").trim();
+ if (!cleaned) {
+ return;
+ }
+ this.syncDirectory = cleaned;
+ ToastUtils.success(this.$t("rns_filesync.folder_selected"));
+ },
+ async openSyncFolder() {
+ const path = String(this.syncDirectory || "").trim();
+ if (!path) {
+ return;
+ }
+ const ok = await ElectronUtils.openDirectoryOrCopy(path, () =>
+ ToastUtils.success(this.$t("rns_filesync.path_copied"))
+ );
+ if (!ok) {
+ ToastUtils.info(path);
+ }
+ },
async refreshAll() {
await Promise.all([this.refreshStatus(), this.refreshPeers(), this.refreshFiles(), this.refreshAcl()]);
},

diff --git a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
index 3ff57cbd..e05dccaf 100644
--- a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
+++ b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
@@ -600,7 +600,7 @@
import MicronParser from "../../js/MicronParser";
import LinkUtils from "../../js/LinkUtils";
import { handleRichHtmlLinkClick } from "../../js/NomadRichHtmlLinks.js";
-import { renderNomadPageByPath, resolveNomadPageShellBackground } from "../../js/NomadPageRenderer";
+import { renderNomadPageByPath, resolveNomadPageShellBackground, isolateNomadLinksInHtml } from "../../js/NomadPageRenderer";
import DialogUtils from "../../js/DialogUtils";
import WebSocketConnection from "../../js/WebSocketConnection";
import NomadNetworkSidebar from "./NomadNetworkSidebar.vue";
@@ -2049,7 +2049,8 @@ export default {
path,
fields,
(pageContent) => {
- const html = muParser.convertMicronToHtml(pageContent, {}, micronOpts);
+ let html = muParser.convertMicronToHtml(pageContent, {}, micronOpts);
+ html = isolateNomadLinksInHtml(html, dest);
const ids = this.partialIdsByKey[key];
if (ids) {
updatePartialDom(html, ids);
@@ -2065,7 +2066,8 @@ export default {
const scheduleRefresh = () => {
this.partialRefreshTimers[key] = setTimeout(() => {
this.downloadNomadNetPage(dest, path, fields, (content) => {
- const h = muParser.convertMicronToHtml(content, {}, micronOpts);
+ let h = muParser.convertMicronToHtml(content, {}, micronOpts);
+ h = isolateNomadLinksInHtml(h, dest);
const idList = this.partialIdsByKey[key];
if (idList) {
updatePartialDom(h, idList);
@@ -2352,7 +2354,8 @@ export default {
if (options === "*") {
useCache = false; // we want to send another request with the field data
- const inputs = document.querySelectorAll(".nodeContainer input, .nodeContainer textarea");
+ // Scope to this tab only. Inactive tabs stay mounted with v-show.
+ const inputs = this.$el.querySelectorAll(".nodeContainer input, .nodeContainer textarea");
const inputValues = {};
@@ -2374,8 +2377,8 @@ export default {
// split options into an array of names
const validNames = options.split("|");
- // Select inputs within the container
- const inputs = document.querySelectorAll(".nodeContainer input, .nodeContainer textarea");
+ // Select inputs within this tab's container only
+ const inputs = this.$el.querySelectorAll(".nodeContainer input, .nodeContainer textarea");
const inputValues = {};
@@ -2397,8 +2400,6 @@ export default {
fieldData = inputValues;
}
- console.log(fieldData);
-
const httpHref = typeof url === "string" ? LinkUtils.httpUrlHrefOrNull(url.trim()) : null;
if (httpHref) {
window.open(httpHref, "_blank", "noopener,noreferrer");
@@ -2430,7 +2431,12 @@ export default {
this.isArchiveDropdownOpen = false;
// use parsed destination hash, or fallback to selected node destination hash
- const destinationHash = parsedUrl.destination_hash || this.selectedNode.destination_hash;
+ const destinationHash =
+ parsedUrl.destination_hash || this.selectedNode?.destination_hash || null;
+ if (!destinationHash) {
+ ToastUtils.warning(this.$t("nomadnet.select_node_to_browse"));
+ return;
+ }
// download file
if (parsedUrl.path.startsWith("/file/")) {

diff --git a/meshchatx/src/frontend/components/relay/RelayChatPage.vue b/meshchatx/src/frontend/components/relay/RelayChatPage.vue
index 59eb5494..b3f97615 100644
--- a/meshchatx/src/frontend/components/relay/RelayChatPage.vue
+++ b/meshchatx/src/frontend/components/relay/RelayChatPage.vue
@@ -2223,7 +2223,11 @@ export default {
if (!this.selectedHub || !this.selectedRoom || !msg) {
return;
}
- const target = this.displayName(msg);
+ // Prefer identity hash so spaced/ambiguous nicks cannot mis-target.
+ const target =
+ typeof msg.src === "string" && /^[a-fA-F0-9]{8,64}$/.test(msg.src.trim())
+ ? msg.src.trim().toLowerCase()
+ : this.displayName(msg);
const room = this.selectedRoom;
const text = template.replace("{room}", room).replace("{target}", target);
try {
@@ -2686,7 +2690,8 @@ export default {
return false;
}
const lowered = text.trim().toLowerCase();
- return lowered.includes("bad key") || lowered.includes("+k");
+ // Match backend: require "bad key" so mode hints like "enable +k" do not wipe storage.
+ return lowered.includes("bad key (+k)") || lowered.includes("bad key");
},
async promptForRoomKey(room) {
const entered = await DialogUtils.prompt(this.$t("relay_chat.room_key_prompt", { room: room || "" }), "", {

diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index d328feee..d9a35cb6 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -2608,8 +2608,8 @@
"description": "IP-Tunneling über Reticulum-Verbindungen (in Entwicklung)."
},
"rns_filesync": {
- "title": "RNS FileSync",
- "description": "Sync a directory with mesh peers over Reticulum using destination hashes."
+ "title": "File Sync",
+ "description": "Keep a shared folder in sync with mesh friends, like Syncthing over Reticulum."
},
"bots": {
"title": "LXMFy-Bots",
@@ -3257,8 +3257,8 @@
"nav_rnsh_desc": "Remote-Shell über Reticulum",
"nav_rnx": "RNX",
"nav_rnx_desc": "Remote-Befehlsausführung über Reticulum",
- "nav_rns_filesync": "RNS FileSync",
- "nav_rns_filesync_desc": "Directory sync over Reticulum"
+ "nav_rns_filesync": "File Sync",
+ "nav_rns_filesync_desc": "Shared folders over the mesh"
},
"settings": {
"shortcut_saved": "Verknüpfung gespeichert",
@@ -3821,55 +3821,89 @@
"windows_screen_security_later": "Nicht jetzt"
},
"rns_filesync": {
- "eyebrow": "Directory sync",
- "title": "RNS FileSync",
- "description": "Sync a local directory with mesh peers over Reticulum. Peers are identity hashes on aspect rns_filesync.filesync.",
- "usage_steps": "How to use FileSync:",
- "step_1": "1. Set a sync directory, start FileSync, and share your destination hash with peers.",
- "step_2": "2. Connect to a peer by identity hash, then browse or download files from the Browse tab.",
- "step_3": "3. Use the ACL tab to grant read, write, or delete permissions when enforcement is enabled.",
- "tab_status": "Status",
- "tab_peers": "Peers",
+ "eyebrow": "Shared folders",
+ "title": "File Sync",
+ "description": "Keep a folder in sync with people on your mesh. Share your sync ID, connect devices, and files move both ways when allowed.",
+ "usage_steps": "Quick start",
+ "step_1": "1. Choose a folder, press Start, then share your sync ID with a friend.",
+ "step_2": "2. On Devices, paste their sync ID to connect. Files appear under Files as they sync.",
+ "step_3": "3. Under Sharing, decide who can read, write, or delete when access control is on.",
+ "tab_folder": "Folder",
+ "tab_devices": "Devices",
"tab_files": "Files",
- "tab_browse": "Browse",
- "tab_acl": "ACL",
- "sync_directory": "Sync directory",
- "sync_directory_placeholder": "Path under this identity storage",
- "announce_interval": "Announce interval (seconds)",
- "monitor": "Watch directory for changes",
- "start": "Start",
- "stop": "Stop",
+ "tab_remote": "Remote",
+ "tab_sharing": "Sharing",
+ "tab_status": "Folder",
+ "tab_peers": "Devices",
+ "tab_browse": "Remote",
+ "tab_acl": "Sharing",
+ "sync_directory": "Folder to sync",
+ "sync_directory_placeholder": "Choose a folder under this identity",
+ "sync_directory_help": "Folders stay inside this identity's MeshChat storage. Use Browse on any device to pick or create one.",
+ "announce_interval": "Announce every (seconds)",
+ "announce_interval_help": "How often others are reminded you are available to sync.",
+ "monitor": "Watch folder for changes",
+ "start": "Start syncing",
+ "stop": "Stop syncing",
"announce": "Announce now",
"refresh": "Refresh",
"running": "Running",
+ "status_syncing": "Syncing",
+ "status_stopped": "Stopped",
"yes": "Yes",
"no": "No",
- "peers_count": "Peers",
+ "peers_count": "Devices",
"files_count": "Files",
- "destination_hash": "Destination hash",
- "last_progress": "Last progress",
- "peer_hash_placeholder": "Identity hash (hex)",
+ "destination_hash": "Sync ID",
+ "share_id": "Your sync ID",
+ "share_id_help": "Send this to friends so they can connect. Tap to copy.",
+ "last_progress": "Latest transfer",
+ "peer_hash_placeholder": "Friend's sync ID",
"connect": "Connect",
- "disconnect": "Disconnect",
- "no_peers": "No connected peers yet.",
- "no_files": "No local files in the sync inventory yet.",
- "select_peer": "Select a peer",
- "browse": "Browse",
- "no_remote_files": "No remote files listed. Connect and browse a peer.",
- "download": "Download",
- "acl_enforce": "Enforce ACL (deny by default)",
- "acl_grant": "Grant",
- "acl_updated": "ACL updated",
- "started": "FileSync started",
- "stopped": "FileSync stopped",
+ "disconnect": "Remove",
+ "no_peers": "No devices connected yet. Start syncing, then paste a friend’s sync ID.",
+ "no_files": "This folder is empty so far. Drop files in, or pull some from Remote.",
+ "select_peer": "Choose a device",
+ "browse": "List files",
+ "no_remote_files": "No remote files yet. Connect a device, then list their files.",
+ "download": "Get file",
+ "acl_enforce": "Only allow listed devices (deny everyone else)",
+ "acl_grant": "Allow device",
+ "acl_updated": "Sharing rules updated",
+ "perm_read": "Read",
+ "perm_write": "Write",
+ "perm_delete": "Delete",
+ "no_acl_rules": "No sharing rules yet. Add a device above when access control is on.",
+ "sharing_help": "Control who may read, change, or delete files in your shared folder.",
+ "devices_help": "Connect to another person’s sync ID. Both sides should be syncing.",
+ "remote_help": "Peek at a connected device’s files and pull one into your folder.",
+ "browse_folder": "Browse",
+ "open_folder": "Open",
+ "browser_title": "Choose sync folder",
+ "browser_hint": "Browse folders for this identity. Works the same on desktop, web, and mobile.",
+ "browser_up": "Up",
+ "browser_loading": "Loading folders...",
+ "browser_empty": "No subfolders here. Create one below, or select this folder.",
+ "browser_new": "New folder",
+ "browser_new_placeholder": "Folder name",
+ "browser_select": "Use this folder",
+ "browser_created": "Folder created",
+ "folder_selected": "Folder selected",
+ "path_copied": "Folder path copied",
+ "stop_before_change_folder": "Stop syncing before changing the folder.",
+ "peer_connected": "Connected",
+ "peer_disconnected": "Disconnected",
+ "peer_unknown": "Unknown",
+ "started": "Syncing started",
+ "stopped": "Syncing stopped",
"announced": "Announce sent",
- "connected": "Peer connect requested",
- "disconnected": "Peer disconnected",
- "browse_done": "Browse complete",
- "download_started": "Download requested",
- "file_updated": "File updated",
- "file_deleted": "File deleted",
+ "connected": "Connecting to device...",
+ "disconnected": "Device removed",
+ "browse_done": "Remote file list ready",
+ "download_started": "Download started",
+ "file_updated": "A file changed",
+ "file_deleted": "A file was removed",
"copied": "Copied to clipboard",
- "error": "FileSync error"
+ "error": "File Sync error"
}
}

diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index f9697d5a..2b09f237 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -2808,8 +2808,8 @@
"description": "Carry IP traffic over Reticulum links (in development)."
},
"rns_filesync": {
- "title": "RNS FileSync",
- "description": "Sync a directory with mesh peers over Reticulum using destination hashes."
+ "title": "File Sync",
+ "description": "Keep a shared folder in sync with mesh friends, like Syncthing over Reticulum."
},
"bots": {
"title": "LXMFy Bots",
@@ -3094,56 +3094,90 @@
"web_save_path_prompt": "Enter the absolute folder path on the machine running MeshChat (server) where the fetched file should be saved:"
},
"rns_filesync": {
- "eyebrow": "Directory sync",
- "title": "RNS FileSync",
- "description": "Sync a local directory with mesh peers over Reticulum. Peers are identity hashes on aspect rns_filesync.filesync.",
- "usage_steps": "How to use FileSync:",
- "step_1": "1. Set a sync directory, start FileSync, and share your destination hash with peers.",
- "step_2": "2. Connect to a peer by identity hash, then browse or download files from the Browse tab.",
- "step_3": "3. Use the ACL tab to grant read, write, or delete permissions when enforcement is enabled.",
- "tab_status": "Status",
- "tab_peers": "Peers",
+ "eyebrow": "Shared folders",
+ "title": "File Sync",
+ "description": "Keep a folder in sync with people on your mesh. Share your sync ID, connect devices, and files move both ways when allowed.",
+ "usage_steps": "Quick start",
+ "step_1": "1. Choose a folder, press Start, then share your sync ID with a friend.",
+ "step_2": "2. On Devices, paste their sync ID to connect. Files appear under Files as they sync.",
+ "step_3": "3. Under Sharing, decide who can read, write, or delete when access control is on.",
+ "tab_folder": "Folder",
+ "tab_devices": "Devices",
"tab_files": "Files",
- "tab_browse": "Browse",
- "tab_acl": "ACL",
- "sync_directory": "Sync directory",
- "sync_directory_placeholder": "Path under this identity storage",
- "announce_interval": "Announce interval (seconds)",
- "monitor": "Watch directory for changes",
- "start": "Start",
- "stop": "Stop",
+ "tab_remote": "Remote",
+ "tab_sharing": "Sharing",
+ "tab_status": "Folder",
+ "tab_peers": "Devices",
+ "tab_browse": "Remote",
+ "tab_acl": "Sharing",
+ "sync_directory": "Folder to sync",
+ "sync_directory_placeholder": "Choose a folder under this identity",
+ "sync_directory_help": "Folders stay inside this identity's MeshChat storage. Use Browse on any device to pick or create one.",
+ "announce_interval": "Announce every (seconds)",
+ "announce_interval_help": "How often others are reminded you are available to sync.",
+ "monitor": "Watch folder for changes",
+ "start": "Start syncing",
+ "stop": "Stop syncing",
"announce": "Announce now",
"refresh": "Refresh",
"running": "Running",
+ "status_syncing": "Syncing",
+ "status_stopped": "Stopped",
"yes": "Yes",
"no": "No",
- "peers_count": "Peers",
+ "peers_count": "Devices",
"files_count": "Files",
- "destination_hash": "Destination hash",
- "last_progress": "Last progress",
- "peer_hash_placeholder": "Identity hash (hex)",
+ "destination_hash": "Sync ID",
+ "share_id": "Your sync ID",
+ "share_id_help": "Send this to friends so they can connect. Tap to copy.",
+ "last_progress": "Latest transfer",
+ "peer_hash_placeholder": "Friend's sync ID",
"connect": "Connect",
- "disconnect": "Disconnect",
- "no_peers": "No connected peers yet.",
- "no_files": "No local files in the sync inventory yet.",
- "select_peer": "Select a peer",
- "browse": "Browse",
- "no_remote_files": "No remote files listed. Connect and browse a peer.",
- "download": "Download",
- "acl_enforce": "Enforce ACL (deny by default)",
- "acl_grant": "Grant",
- "acl_updated": "ACL updated",
- "started": "FileSync started",
- "stopped": "FileSync stopped",
+ "disconnect": "Remove",
+ "no_peers": "No devices connected yet. Start syncing, then paste a friend’s sync ID.",
+ "no_files": "This folder is empty so far. Drop files in, or pull some from Remote.",
+ "select_peer": "Choose a device",
+ "browse": "List files",
+ "no_remote_files": "No remote files yet. Connect a device, then list their files.",
+ "download": "Get file",
+ "acl_enforce": "Only allow listed devices (deny everyone else)",
+ "acl_grant": "Allow device",
+ "acl_updated": "Sharing rules updated",
+ "perm_read": "Read",
+ "perm_write": "Write",
+ "perm_delete": "Delete",
+ "no_acl_rules": "No sharing rules yet. Add a device above when access control is on.",
+ "sharing_help": "Control who may read, change, or delete files in your shared folder.",
+ "devices_help": "Connect to another person’s sync ID. Both sides should be syncing.",
+ "remote_help": "Peek at a connected device’s files and pull one into your folder.",
+ "browse_folder": "Browse",
+ "open_folder": "Open",
+ "browser_title": "Choose sync folder",
+ "browser_hint": "Browse folders for this identity. Works the same on desktop, web, and mobile.",
+ "browser_up": "Up",
+ "browser_loading": "Loading folders...",
+ "browser_empty": "No subfolders here. Create one below, or select this folder.",
+ "browser_new": "New folder",
+ "browser_new_placeholder": "Folder name",
+ "browser_select": "Use this folder",
+ "browser_created": "Folder created",
+ "folder_selected": "Folder selected",
+ "path_copied": "Folder path copied",
+ "stop_before_change_folder": "Stop syncing before changing the folder.",
+ "peer_connected": "Connected",
+ "peer_disconnected": "Disconnected",
+ "peer_unknown": "Unknown",
+ "started": "Syncing started",
+ "stopped": "Syncing stopped",
"announced": "Announce sent",
- "connected": "Peer connect requested",
- "disconnected": "Peer disconnected",
- "browse_done": "Browse complete",
- "download_started": "Download requested",
- "file_updated": "File updated",
- "file_deleted": "File deleted",
+ "connected": "Connecting to device...",
+ "disconnected": "Device removed",
+ "browse_done": "Remote file list ready",
+ "download_started": "Download started",
+ "file_updated": "A file changed",
+ "file_deleted": "A file was removed",
"copied": "Copied to clipboard",
- "error": "FileSync error"
+ "error": "File Sync error"
},
"rnsh": {
"remote_shell": "Remote shell",
@@ -3830,8 +3864,8 @@
"nav_rnprobe_desc": "Probe reachability and delay",
"nav_rncp": "RNCP",
"nav_rncp_desc": "File copy over Reticulum (RNCP)",
- "nav_rns_filesync": "RNS FileSync",
- "nav_rns_filesync_desc": "Directory sync over Reticulum",
+ "nav_rns_filesync": "File Sync",
+ "nav_rns_filesync_desc": "Shared folders over the mesh",
"nav_rnsh": "RNSH",
"nav_rnsh_desc": "Remote shell over Reticulum",
"nav_rnx": "RNX",

diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index ba53d635..8d25a799 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -2804,8 +2804,8 @@
"description": "Tunelización IP sobre los enlaces de Reticulum (en desarrollo)."
},
"rns_filesync": {
- "title": "RNS FileSync",
- "description": "Sync a directory with mesh peers over Reticulum using destination hashes."
+ "title": "File Sync",
+ "description": "Keep a shared folder in sync with mesh friends, like Syncthing over Reticulum."
},
"bots": {
"title": "Bots LXMFy",
@@ -3505,8 +3505,8 @@
"nav_rnsh_desc": "Shell remoto sobre Reticulum",
"nav_rnx": "RNX",
"nav_rnx_desc": "Ejecución remota de comandos sobre Reticulum",
- "nav_rns_filesync": "RNS FileSync",
- "nav_rns_filesync_desc": "Directory sync over Reticulum"
+ "nav_rns_filesync": "File Sync",
+ "nav_rns_filesync_desc": "Shared folders over the mesh"
},
"relay_chat": {
"title": "Chat de retransmision",
@@ -3821,55 +3821,89 @@
"windows_screen_security_later": "Ahora no"
},
"rns_filesync": {
- "eyebrow": "Directory sync",
- "title": "RNS FileSync",
- "description": "Sync a local directory with mesh peers over Reticulum. Peers are identity hashes on aspect rns_filesync.filesync.",
- "usage_steps": "How to use FileSync:",
- "step_1": "1. Set a sync directory, start FileSync, and share your destination hash with peers.",
- "step_2": "2. Connect to a peer by identity hash, then browse or download files from the Browse tab.",
- "step_3": "3. Use the ACL tab to grant read, write, or delete permissions when enforcement is enabled.",
- "tab_status": "Status",
- "tab_peers": "Peers",
+ "eyebrow": "Shared folders",
+ "title": "File Sync",
+ "description": "Keep a folder in sync with people on your mesh. Share your sync ID, connect devices, and files move both ways when allowed.",
+ "usage_steps": "Quick start",
+ "step_1": "1. Choose a folder, press Start, then share your sync ID with a friend.",
+ "step_2": "2. On Devices, paste their sync ID to connect. Files appear under Files as they sync.",
+ "step_3": "3. Under Sharing, decide who can read, write, or delete when access control is on.",
+ "tab_folder": "Folder",
+ "tab_devices": "Devices",
"tab_files": "Files",
- "tab_browse": "Browse",
- "tab_acl": "ACL",
- "sync_directory": "Sync directory",
- "sync_directory_placeholder": "Path under this identity storage",
- "announce_interval": "Announce interval (seconds)",
- "monitor": "Watch directory for changes",
- "start": "Start",
- "stop": "Stop",
+ "tab_remote": "Remote",
+ "tab_sharing": "Sharing",
+ "tab_status": "Folder",
+ "tab_peers": "Devices",
+ "tab_browse": "Remote",
+ "tab_acl": "Sharing",
+ "sync_directory": "Folder to sync",
+ "sync_directory_placeholder": "Choose a folder under this identity",
+ "sync_directory_help": "Folders stay inside this identity's MeshChat storage. Use Browse on any device to pick or create one.",
+ "announce_interval": "Announce every (seconds)",
+ "announce_interval_help": "How often others are reminded you are available to sync.",
+ "monitor": "Watch folder for changes",
+ "start": "Start syncing",
+ "stop": "Stop syncing",
"announce": "Announce now",
"refresh": "Refresh",
"running": "Running",
+ "status_syncing": "Syncing",
+ "status_stopped": "Stopped",
"yes": "Yes",
"no": "No",
- "peers_count": "Peers",
+ "peers_count": "Devices",
"files_count": "Files",
- "destination_hash": "Destination hash",
- "last_progress": "Last progress",
- "peer_hash_placeholder": "Identity hash (hex)",
+ "destination_hash": "Sync ID",
+ "share_id": "Your sync ID",
+ "share_id_help": "Send this to friends so they can connect. Tap to copy.",
+ "last_progress": "Latest transfer",
+ "peer_hash_placeholder": "Friend's sync ID",
"connect": "Connect",
- "disconnect": "Disconnect",
- "no_peers": "No connected peers yet.",
- "no_files": "No local files in the sync inventory yet.",
- "select_peer": "Select a peer",
- "browse": "Browse",
- "no_remote_files": "No remote files listed. Connect and browse a peer.",
- "download": "Download",
- "acl_enforce": "Enforce ACL (deny by default)",
- "acl_grant": "Grant",
- "acl_updated": "ACL updated",
- "started": "FileSync started",
- "stopped": "FileSync stopped",
+ "disconnect": "Remove",
+ "no_peers": "No devices connected yet. Start syncing, then paste a friend’s sync ID.",
+ "no_files": "This folder is empty so far. Drop files in, or pull some from Remote.",
+ "select_peer": "Choose a device",
+ "browse": "List files",
+ "no_remote_files": "No remote files yet. Connect a device, then list their files.",
+ "download": "Get file",
+ "acl_enforce": "Only allow listed devices (deny everyone else)",
+ "acl_grant": "Allow device",
+ "acl_updated": "Sharing rules updated",
+ "perm_read": "Read",
+ "perm_write": "Write",
+ "perm_delete": "Delete",
+ "no_acl_rules": "No sharing rules yet. Add a device above when access control is on.",
+ "sharing_help": "Control who may read, change, or delete files in your shared folder.",
+ "devices_help": "Connect to another person’s sync ID. Both sides should be syncing.",
+ "remote_help": "Peek at a connected device’s files and pull one into your folder.",
+ "browse_folder": "Browse",
+ "open_folder": "Open",
+ "browser_title": "Choose sync folder",
+ "browser_hint": "Browse folders for this identity. Works the same on desktop, web, and mobile.",
+ "browser_up": "Up",
+ "browser_loading": "Loading folders...",
+ "browser_empty": "No subfolders here. Create one below, or select this folder.",
+ "browser_new": "New folder",
+ "browser_new_placeholder": "Folder name",
+ "browser_select": "Use this folder",
+ "browser_created": "Folder created",
+ "folder_selected": "Folder selected",
+ "path_copied": "Folder path copied",
+ "stop_before_change_folder": "Stop syncing before changing the folder.",
+ "peer_connected": "Connected",
+ "peer_disconnected": "Disconnected",
+ "peer_unknown": "Unknown",
+ "started": "Syncing started",
+ "stopped": "Syncing stopped",
"announced": "Announce sent",
- "connected": "Peer connect requested",
- "disconnected": "Peer disconnected",
- "browse_done": "Browse complete",
- "download_started": "Download requested",
- "file_updated": "File updated",
- "file_deleted": "File deleted",
+ "connected": "Connecting to device...",
+ "disconnected": "Device removed",
+ "browse_done": "Remote file list ready",
+ "download_started": "Download started",
+ "file_updated": "A file changed",
+ "file_deleted": "A file was removed",
"copied": "Copied to clipboard",
- "error": "FileSync error"
+ "error": "File Sync error"
}
}

diff --git a/meshchatx/src/frontend/locales/fi.json b/meshchatx/src/frontend/locales/fi.json
index 2e37269f..920138a5 100644
--- a/meshchatx/src/frontend/locales/fi.json
+++ b/meshchatx/src/frontend/locales/fi.json
@@ -2804,8 +2804,8 @@
"description": "Kuljeta IP-liikennettä Reticulum-linkkien yli (kehitysvaiheessa)."
},
"rns_filesync": {
- "title": "RNS FileSync",
- "description": "Sync a directory with mesh peers over Reticulum using destination hashes."
+ "title": "File Sync",
+ "description": "Keep a shared folder in sync with mesh friends, like Syncthing over Reticulum."
},
"bots": {
"title": "LXMFy-botit",
@@ -3727,8 +3727,8 @@
"nav_rnsh_desc": "Etäkuori Reticulumin yli",
"nav_rnx": "RNX",
"nav_rnx_desc": "Etäkomentojen suoritus Reticulumin yli",
- "nav_rns_filesync": "RNS FileSync",
- "nav_rns_filesync_desc": "Directory sync over Reticulum"
+ "nav_rns_filesync": "File Sync",
+ "nav_rns_filesync_desc": "Shared folders over the mesh"
},
"rnx": {
"title": "RNX - Reticulum Remote Execution",
@@ -3821,55 +3821,89 @@
"windows_screen_security_later": "Ei nyt"
},
"rns_filesync": {
- "eyebrow": "Directory sync",
- "title": "RNS FileSync",
- "description": "Sync a local directory with mesh peers over Reticulum. Peers are identity hashes on aspect rns_filesync.filesync.",
- "usage_steps": "How to use FileSync:",
- "step_1": "1. Set a sync directory, start FileSync, and share your destination hash with peers.",
- "step_2": "2. Connect to a peer by identity hash, then browse or download files from the Browse tab.",
- "step_3": "3. Use the ACL tab to grant read, write, or delete permissions when enforcement is enabled.",
- "tab_status": "Status",
- "tab_peers": "Peers",
+ "eyebrow": "Shared folders",
+ "title": "File Sync",
+ "description": "Keep a folder in sync with people on your mesh. Share your sync ID, connect devices, and files move both ways when allowed.",
+ "usage_steps": "Quick start",
+ "step_1": "1. Choose a folder, press Start, then share your sync ID with a friend.",
+ "step_2": "2. On Devices, paste their sync ID to connect. Files appear under Files as they sync.",
+ "step_3": "3. Under Sharing, decide who can read, write, or delete when access control is on.",
+ "tab_folder": "Folder",
+ "tab_devices": "Devices",
"tab_files": "Files",
- "tab_browse": "Browse",
- "tab_acl": "ACL",
- "sync_directory": "Sync directory",
- "sync_directory_placeholder": "Path under this identity storage",
- "announce_interval": "Announce interval (seconds)",
- "monitor": "Watch directory for changes",
- "start": "Start",
- "stop": "Stop",
+ "tab_remote": "Remote",
+ "tab_sharing": "Sharing",
+ "tab_status": "Folder",
+ "tab_peers": "Devices",
+ "tab_browse": "Remote",
+ "tab_acl": "Sharing",
+ "sync_directory": "Folder to sync",
+ "sync_directory_placeholder": "Choose a folder under this identity",
+ "sync_directory_help": "Folders stay inside this identity's MeshChat storage. Use Browse on any device to pick or create one.",
+ "announce_interval": "Announce every (seconds)",
+ "announce_interval_help": "How often others are reminded you are available to sync.",
+ "monitor": "Watch folder for changes",
+ "start": "Start syncing",
+ "stop": "Stop syncing",
"announce": "Announce now",
"refresh": "Refresh",
"running": "Running",
+ "status_syncing": "Syncing",
+ "status_stopped": "Stopped",
"yes": "Yes",
"no": "No",
- "peers_count": "Peers",
+ "peers_count": "Devices",
"files_count": "Files",
- "destination_hash": "Destination hash",
- "last_progress": "Last progress",
- "peer_hash_placeholder": "Identity hash (hex)",
+ "destination_hash": "Sync ID",
+ "share_id": "Your sync ID",
+ "share_id_help": "Send this to friends so they can connect. Tap to copy.",
+ "last_progress": "Latest transfer",
+ "peer_hash_placeholder": "Friend's sync ID",
"connect": "Connect",
- "disconnect": "Disconnect",
- "no_peers": "No connected peers yet.",
- "no_files": "No local files in the sync inventory yet.",
- "select_peer": "Select a peer",
- "browse": "Browse",
- "no_remote_files": "No remote files listed. Connect and browse a peer.",
- "download": "Download",
- "acl_enforce": "Enforce ACL (deny by default)",
- "acl_grant": "Grant",
- "acl_updated": "ACL updated",
- "started": "FileSync started",
- "stopped": "FileSync stopped",
+ "disconnect": "Remove",
+ "no_peers": "No devices connected yet. Start syncing, then paste a friend’s sync ID.",
+ "no_files": "This folder is empty so far. Drop files in, or pull some from Remote.",
+ "select_peer": "Choose a device",
+ "browse": "List files",
+ "no_remote_files": "No remote files yet. Connect a device, then list their files.",
+ "download": "Get file",
+ "acl_enforce": "Only allow listed devices (deny everyone else)",
+ "acl_grant": "Allow device",
+ "acl_updated": "Sharing rules updated",
+ "perm_read": "Read",
+ "perm_write": "Write",
+ "perm_delete": "Delete",
+ "no_acl_rules": "No sharing rules yet. Add a device above when access control is on.",
+ "sharing_help": "Control who may read, change, or delete files in your shared folder.",
+ "devices_help": "Connect to another person’s sync ID. Both sides should be syncing.",
+ "remote_help": "Peek at a connected device’s files and pull one into your folder.",
+ "browse_folder": "Browse",
+ "open_folder": "Open",
+ "browser_title": "Choose sync folder",
+ "browser_hint": "Browse folders for this identity. Works the same on desktop, web, and mobile.",
+ "browser_up": "Up",
+ "browser_loading": "Loading folders...",
+ "browser_empty": "No subfolders here. Create one below, or select this folder.",
+ "browser_new": "New folder",
+ "browser_new_placeholder": "Folder name",
+ "browser_select": "Use this folder",
+ "browser_created": "Folder created",
+ "folder_selected": "Folder selected",
+ "path_copied": "Folder path copied",
+ "stop_before_change_folder": "Stop syncing before changing the folder.",
+ "peer_connected": "Connected",
+ "peer_disconnected": "Disconnected",
+ "peer_unknown": "Unknown",
+ "started": "Syncing started",
+ "stopped": "Syncing stopped",
"announced": "Announce sent",
- "connected": "Peer connect requested",
- "disconnected": "Peer disconnected",
- "browse_done": "Browse complete",
- "download_started": "Download requested",
- "file_updated": "File updated",
- "file_deleted": "File deleted",
+ "connected": "Connecting to device...",
+ "disconnected": "Device removed",
+ "browse_done": "Remote file list ready",
+ "download_started": "Download started",
+ "file_updated": "A file changed",
+ "file_deleted": "A file was removed",
"copied": "Copied to clipboard",
- "error": "FileSync error"
+ "error": "File Sync error"
}
}

diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index a832faf0..c2b7a371 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -2804,8 +2804,8 @@
"description": "Tunnel IP sur les liaisons Reticulum (en développement)."
},
"rns_filesync": {
- "title": "RNS FileSync",
- "description": "Sync a directory with mesh peers over Reticulum using destination hashes."
+ "title": "File Sync",
+ "description": "Keep a shared folder in sync with mesh friends, like Syncthing over Reticulum."
},
"bots": {
"title": "Bots LXMFy",
@@ -3505,8 +3505,8 @@
"nav_rnsh_desc": "Shell distant sur Reticulum",
"nav_rnx": "RNX",
"nav_rnx_desc": "Exécution distante de commandes sur Reticulum",
- "nav_rns_filesync": "RNS FileSync",
- "nav_rns_filesync_desc": "Directory sync over Reticulum"
+ "nav_rns_filesync": "File Sync",
+ "nav_rns_filesync_desc": "Shared folders over the mesh"
},
"relay_chat": {
"title": "Chat relais",
@@ -3821,55 +3821,89 @@
"windows_screen_security_later": "Pas maintenant"
},
"rns_filesync": {
- "eyebrow": "Directory sync",
- "title": "RNS FileSync",
- "description": "Sync a local directory with mesh peers over Reticulum. Peers are identity hashes on aspect rns_filesync.filesync.",
- "usage_steps": "How to use FileSync:",
- "step_1": "1. Set a sync directory, start FileSync, and share your destination hash with peers.",
- "step_2": "2. Connect to a peer by identity hash, then browse or download files from the Browse tab.",
- "step_3": "3. Use the ACL tab to grant read, write, or delete permissions when enforcement is enabled.",
- "tab_status": "Status",
- "tab_peers": "Peers",
+ "eyebrow": "Shared folders",
+ "title": "File Sync",
+ "description": "Keep a folder in sync with people on your mesh. Share your sync ID, connect devices, and files move both ways when allowed.",
+ "usage_steps": "Quick start",
+ "step_1": "1. Choose a folder, press Start, then share your sync ID with a friend.",
+ "step_2": "2. On Devices, paste their sync ID to connect. Files appear under Files as they sync.",
+ "step_3": "3. Under Sharing, decide who can read, write, or delete when access control is on.",
+ "tab_folder": "Folder",
+ "tab_devices": "Devices",
"tab_files": "Files",
- "tab_browse": "Browse",
- "tab_acl": "ACL",
- "sync_directory": "Sync directory",
- "sync_directory_placeholder": "Path under this identity storage",
- "announce_interval": "Announce interval (seconds)",
- "monitor": "Watch directory for changes",
- "start": "Start",
- "stop": "Stop",
+ "tab_remote": "Remote",
+ "tab_sharing": "Sharing",
+ "tab_status": "Folder",
+ "tab_peers": "Devices",
+ "tab_browse": "Remote",
+ "tab_acl": "Sharing",
+ "sync_directory": "Folder to sync",
+ "sync_directory_placeholder": "Choose a folder under this identity",
+ "sync_directory_help": "Folders stay inside this identity's MeshChat storage. Use Browse on any device to pick or create one.",
+ "announce_interval": "Announce every (seconds)",
+ "announce_interval_help": "How often others are reminded you are available to sync.",
+ "monitor": "Watch folder for changes",
+ "start": "Start syncing",
+ "stop": "Stop syncing",
"announce": "Announce now",
"refresh": "Refresh",
"running": "Running",
+ "status_syncing": "Syncing",
+ "status_stopped": "Stopped",
"yes": "Yes",
"no": "No",
- "peers_count": "Peers",
+ "peers_count": "Devices",
"files_count": "Files",
- "destination_hash": "Destination hash",
- "last_progress": "Last progress",
- "peer_hash_placeholder": "Identity hash (hex)",
+ "destination_hash": "Sync ID",
+ "share_id": "Your sync ID",
+ "share_id_help": "Send this to friends so they can connect. Tap to copy.",
+ "last_progress": "Latest transfer",
+ "peer_hash_placeholder": "Friend's sync ID",
"connect": "Connect",
- "disconnect": "Disconnect",
- "no_peers": "No connected peers yet.",
- "no_files": "No local files in the sync inventory yet.",
- "select_peer": "Select a peer",
- "browse": "Browse",
- "no_remote_files": "No remote files listed. Connect and browse a peer.",
- "download": "Download",
- "acl_enforce": "Enforce ACL (deny by default)",
- "acl_grant": "Grant",
- "acl_updated": "ACL updated",
- "started": "FileSync started",
- "stopped": "FileSync stopped",
+ "disconnect": "Remove",
+ "no_peers": "No devices connected yet. Start syncing, then paste a friend’s sync ID.",
+ "no_files": "This folder is empty so far. Drop files in, or pull some from Remote.",
+ "select_peer": "Choose a device",
+ "browse": "List files",
+ "no_remote_files": "No remote files yet. Connect a device, then list their files.",
+ "download": "Get file",
+ "acl_enforce": "Only allow listed devices (deny everyone else)",
+ "acl_grant": "Allow device",
+ "acl_updated": "Sharing rules updated",
+ "perm_read": "Read",
+ "perm_write": "Write",
+ "perm_delete": "Delete",
+ "no_acl_rules": "No sharing rules yet. Add a device above when access control is on.",
+ "sharing_help": "Control who may read, change, or delete files in your shared folder.",
+ "devices_help": "Connect to another person’s sync ID. Both sides should be syncing.",
+ "remote_help": "Peek at a connected device’s files and pull one into your folder.",
+ "browse_folder": "Browse",
+ "open_folder": "Open",
+ "browser_title": "Choose sync folder",
+ "browser_hint": "Browse folders for this identity. Works the same on desktop, web, and mobile.",
+ "browser_up": "Up",
+ "browser_loading": "Loading folders...",
+ "browser_empty": "No subfolders here. Create one below, or select this folder.",
+ "browser_new": "New folder",
+ "browser_new_placeholder": "Folder name",
+ "browser_select": "Use this folder",
+ "browser_created": "Folder created",
+ "folder_selected": "Folder selected",
+ "path_copied": "Folder path copied",
+ "stop_before_change_folder": "Stop syncing before changing the folder.",
+ "peer_connected": "Connected",
+ "peer_disconnected": "Disconnected",
+ "peer_unknown": "Unknown",
+ "started": "Syncing started",
+ "stopped": "Syncing stopped",
"announced": "Announce sent",
- "connected": "Peer connect requested",
- "disconnected": "Peer disconnected",
- "browse_done": "Browse complete",
- "download_started": "Download requested",
- "file_updated": "File updated",
- "file_deleted": "File deleted",
+ "connected": "Connecting to device...",
+ "disconnected": "Device removed",
+ "browse_done": "Remote file list ready",
+ "download_started": "Download started",
+ "file_updated": "A file changed",
+ "file_deleted": "A file was removed",
"copied": "Copied to clipboard",
- "error": "FileSync error"
+ "error": "File Sync error"
}
}

diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index 31f92c02..71241d3c 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -2856,8 +2856,8 @@
"description": "Tunnel IP su collegamenti Reticulum (in sviluppo)."
},
"rns_filesync": {
- "title": "RNS FileSync",
- "description": "Sync a directory with mesh peers over Reticulum using destination hashes."
+ "title": "File Sync",
+ "description": "Keep a shared folder in sync with mesh friends, like Syncthing over Reticulum."
},
"bots": {
"title": "LXMFy Bot",
@@ -3505,8 +3505,8 @@
"nav_rnsh_desc": "Shell remota su Reticulum",
"nav_rnx": "RNX",
"nav_rnx_desc": "Esecuzione remota di comandi su Reticulum",
- "nav_rns_filesync": "RNS FileSync",
- "nav_rns_filesync_desc": "Directory sync over Reticulum"
+ "nav_rns_filesync": "File Sync",
+ "nav_rns_filesync_desc": "Shared folders over the mesh"
},
"relay_chat": {
"title": "Chat relay",
@@ -3821,55 +3821,89 @@
"windows_screen_security_later": "Non ora"
},
"rns_filesync": {
- "eyebrow": "Directory sync",
- "title": "RNS FileSync",
- "description": "Sync a local directory with mesh peers over Reticulum. Peers are identity hashes on aspect rns_filesync.filesync.",
- "usage_steps": "How to use FileSync:",
- "step_1": "1. Set a sync directory, start FileSync, and share your destination hash with peers.",
- "step_2": "2. Connect to a peer by identity hash, then browse or download files from the Browse tab.",
- "step_3": "3. Use the ACL tab to grant read, write, or delete permissions when enforcement is enabled.",
- "tab_status": "Status",
- "tab_peers": "Peers",
+ "eyebrow": "Shared folders",
+ "title": "File Sync",
+ "description": "Keep a folder in sync with people on your mesh. Share your sync ID, connect devices, and files move both ways when allowed.",
+ "usage_steps": "Quick start",
+ "step_1": "1. Choose a folder, press Start, then share your sync ID with a friend.",
+ "step_2": "2. On Devices, paste their sync ID to connect. Files appear under Files as they sync.",
+ "step_3": "3. Under Sharing, decide who can read, write, or delete when access control is on.",
+ "tab_folder": "Folder",
+ "tab_devices": "Devices",
"tab_files": "Files",
- "tab_browse": "Browse",
- "tab_acl": "ACL",
- "sync_directory": "Sync directory",
- "sync_directory_placeholder": "Path under this identity storage",
- "announce_interval": "Announce interval (seconds)",
- "monitor": "Watch directory for changes",
- "start": "Start",
- "stop": "Stop",
+ "tab_remote": "Remote",
+ "tab_sharing": "Sharing",
+ "tab_status": "Folder",
+ "tab_peers": "Devices",
+ "tab_browse": "Remote",
+ "tab_acl": "Sharing",
+ "sync_directory": "Folder to sync",
+ "sync_directory_placeholder": "Choose a folder under this identity",
+ "sync_directory_help": "Folders stay inside this identity's MeshChat storage. Use Browse on any device to pick or create one.",
+ "announce_interval": "Announce every (seconds)",
+ "announce_interval_help": "How often others are reminded you are available to sync.",
+ "monitor": "Watch folder for changes",
+ "start": "Start syncing",
+ "stop": "Stop syncing",
"announce": "Announce now",
"refresh": "Refresh",
"running": "Running",
+ "status_syncing": "Syncing",
+ "status_stopped": "Stopped",
"yes": "Yes",
"no": "No",
- "peers_count": "Peers",
+ "peers_count": "Devices",
"files_count": "Files",
- "destination_hash": "Destination hash",
- "last_progress": "Last progress",
- "peer_hash_placeholder": "Identity hash (hex)",
+ "destination_hash": "Sync ID",
+ "share_id": "Your sync ID",
+ "share_id_help": "Send this to friends so they can connect. Tap to copy.",
+ "last_progress": "Latest transfer",
+ "peer_hash_placeholder": "Friend's sync ID",
"connect": "Connect",
- "disconnect": "Disconnect",
- "no_peers": "No connected peers yet.",
- "no_files": "No local files in the sync inventory yet.",
- "select_peer": "Select a peer",
- "browse": "Browse",
- "no_remote_files": "No remote files listed. Connect and browse a peer.",
- "download": "Download",
- "acl_enforce": "Enforce ACL (deny by default)",
- "acl_grant": "Grant",
- "acl_updated": "ACL updated",
- "started": "FileSync started",
- "stopped": "FileSync stopped",
+ "disconnect": "Remove",
+ "no_peers": "No devices connected yet. Start syncing, then paste a friend’s sync ID.",
+ "no_files": "This folder is empty so far. Drop files in, or pull some from Remote.",
+ "select_peer": "Choose a device",
+ "browse": "List files",
+ "no_remote_files": "No remote files yet. Connect a device, then list their files.",
+ "download": "Get file",
+ "acl_enforce": "Only allow listed devices (deny everyone else)",
+ "acl_grant": "Allow device",
+ "acl_updated": "Sharing rules updated",
+ "perm_read": "Read",
+ "perm_write": "Write",
+ "perm_delete": "Delete",
+ "no_acl_rules": "No sharing rules yet. Add a device above when access control is on.",
+ "sharing_help": "Control who may read, change, or delete files in your shared folder.",
+ "devices_help": "Connect to another person’s sync ID. Both sides should be syncing.",
+ "remote_help": "Peek at a connected device’s files and pull one into your folder.",
+ "browse_folder": "Browse",
+ "open_folder": "Open",
+ "browser_title": "Choose sync folder",
+ "browser_hint": "Browse folders for this identity. Works the same on desktop, web, and mobile.",
+ "browser_up": "Up",
+ "browser_loading": "Loading folders...",
+ "browser_empty": "No subfolders here. Create one below, or select this folder.",
+ "browser_new": "New folder",
+ "browser_new_placeholder": "Folder name",
+ "browser_select": "Use this folder",
+ "browser_created": "Folder created",
+ "folder_selected": "Folder selected",
+ "path_copied": "Folder path copied",
+ "stop_before_change_folder": "Stop syncing before changing the folder.",
+ "peer_connected": "Connected",
+ "peer_disconnected": "Disconnected",
+ "peer_unknown": "Unknown",
+ "started": "Syncing started",
+ "stopped": "Syncing stopped",
"announced": "Announce sent",
- "connected": "Peer connect requested",
- "disconnected": "Peer disconnected",
- "browse_done": "Browse complete",
- "download_started": "Download requested",
- "file_updated": "File updated",
- "file_deleted": "File deleted",
+ "connected": "Connecting to device...",
+ "disconnected": "Device removed",
+ "browse_done": "Remote file list ready",
+ "download_started": "Download started",
+ "file_updated": "A file changed",
+ "file_deleted": "A file was removed",
"copied": "Copied to clipboard",
- "error": "FileSync error"
+ "error": "File Sync error"
}
}

diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index 14375356..4b222b7f 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -2804,8 +2804,8 @@
"description": "IP tunneling over Reticulum links (in ontwikkeling)."
},
"rns_filesync": {
- "title": "RNS FileSync",
- "description": "Sync a directory with mesh peers over Reticulum using destination hashes."
+ "title": "File Sync",
+ "description": "Keep a shared folder in sync with mesh friends, like Syncthing over Reticulum."
},
"bots": {
"title": "LXMFy Bots",
@@ -3505,8 +3505,8 @@
"nav_rnsh_desc": "Remote shell over Reticulum",
"nav_rnx": "RNX",
"nav_rnx_desc": "Externe opdrachtuitvoering over Reticulum",
- "nav_rns_filesync": "RNS FileSync",
- "nav_rns_filesync_desc": "Directory sync over Reticulum"
+ "nav_rns_filesync": "File Sync",
+ "nav_rns_filesync_desc": "Shared folders over the mesh"
},
"relay_chat": {
"title": "Relaychat",
@@ -3821,55 +3821,89 @@
"windows_screen_security_later": "Niet nu"
},
"rns_filesync": {
- "eyebrow": "Directory sync",
- "title": "RNS FileSync",
- "description": "Sync a local directory with mesh peers over Reticulum. Peers are identity hashes on aspect rns_filesync.filesync.",
- "usage_steps": "How to use FileSync:",
- "step_1": "1. Set a sync directory, start FileSync, and share your destination hash with peers.",
- "step_2": "2. Connect to a peer by identity hash, then browse or download files from the Browse tab.",
- "step_3": "3. Use the ACL tab to grant read, write, or delete permissions when enforcement is enabled.",
- "tab_status": "Status",
- "tab_peers": "Peers",
+ "eyebrow": "Shared folders",
+ "title": "File Sync",
+ "description": "Keep a folder in sync with people on your mesh. Share your sync ID, connect devices, and files move both ways when allowed.",
+ "usage_steps": "Quick start",
+ "step_1": "1. Choose a folder, press Start, then share your sync ID with a friend.",
+ "step_2": "2. On Devices, paste their sync ID to connect. Files appear under Files as they sync.",
+ "step_3": "3. Under Sharing, decide who can read, write, or delete when access control is on.",
+ "tab_folder": "Folder",
+ "tab_devices": "Devices",
"tab_files": "Files",
- "tab_browse": "Browse",
- "tab_acl": "ACL",
- "sync_directory": "Sync directory",
- "sync_directory_placeholder": "Path under this identity storage",
- "announce_interval": "Announce interval (seconds)",
- "monitor": "Watch directory for changes",
- "start": "Start",
- "stop": "Stop",
+ "tab_remote": "Remote",
+ "tab_sharing": "Sharing",
+ "tab_status": "Folder",
+ "tab_peers": "Devices",
+ "tab_browse": "Remote",
+ "tab_acl": "Sharing",
+ "sync_directory": "Folder to sync",
+ "sync_directory_placeholder": "Choose a folder under this identity",
+ "sync_directory_help": "Folders stay inside this identity's MeshChat storage. Use Browse on any device to pick or create one.",
+ "announce_interval": "Announce every (seconds)",
+ "announce_interval_help": "How often others are reminded you are available to sync.",
+ "monitor": "Watch folder for changes",
+ "start": "Start syncing",
+ "stop": "Stop syncing",
"announce": "Announce now",
"refresh": "Refresh",
"running": "Running",
+ "status_syncing": "Syncing",
+ "status_stopped": "Stopped",
"yes": "Yes",
"no": "No",
- "peers_count": "Peers",
+ "peers_count": "Devices",
"files_count": "Files",
- "destination_hash": "Destination hash",
- "last_progress": "Last progress",
- "peer_hash_placeholder": "Identity hash (hex)",
+ "destination_hash": "Sync ID",
+ "share_id": "Your sync ID",
+ "share_id_help": "Send this to friends so they can connect. Tap to copy.",
+ "last_progress": "Latest transfer",
+ "peer_hash_placeholder": "Friend's sync ID",
"connect": "Connect",
- "disconnect": "Disconnect",
- "no_peers": "No connected peers yet.",
- "no_files": "No local files in the sync inventory yet.",
- "select_peer": "Select a peer",
- "browse": "Browse",
- "no_remote_files": "No remote files listed. Connect and browse a peer.",
- "download": "Download",
- "acl_enforce": "Enforce ACL (deny by default)",
- "acl_grant": "Grant",
- "acl_updated": "ACL updated",
- "started": "FileSync started",
- "stopped": "FileSync stopped",
+ "disconnect": "Remove",
+ "no_peers": "No devices connected yet. Start syncing, then paste a friend’s sync ID.",
+ "no_files": "This folder is empty so far. Drop files in, or pull some from Remote.",
+ "select_peer": "Choose a device",
+ "browse": "List files",
+ "no_remote_files": "No remote files yet. Connect a device, then list their files.",
+ "download": "Get file",
+ "acl_enforce": "Only allow listed devices (deny everyone else)",
+ "acl_grant": "Allow device",
+ "acl_updated": "Sharing rules updated",
+ "perm_read": "Read",
+ "perm_write": "Write",
+ "perm_delete": "Delete",
+ "no_acl_rules": "No sharing rules yet. Add a device above when access control is on.",
+ "sharing_help": "Control who may read, change, or delete files in your shared folder.",
+ "devices_help": "Connect to another person’s sync ID. Both sides should be syncing.",
+ "remote_help": "Peek at a connected device’s files and pull one into your folder.",
+ "browse_folder": "Browse",
+ "open_folder": "Open",
+ "browser_title": "Choose sync folder",
+ "browser_hint": "Browse folders for this identity. Works the same on desktop, web, and mobile.",
+ "browser_up": "Up",
+ "browser_loading": "Loading folders...",
+ "browser_empty": "No subfolders here. Create one below, or select this folder.",
+ "browser_new": "New folder",
+ "browser_new_placeholder": "Folder name",
+ "browser_select": "Use this folder",
+ "browser_created": "Folder created",
+ "folder_selected": "Folder selected",
+ "path_copied": "Folder path copied",
+ "stop_before_change_folder": "Stop syncing before changing the folder.",
+ "peer_connected": "Connected",
+ "peer_disconnected": "Disconnected",
+ "peer_unknown": "Unknown",
+ "started": "Syncing started",
+ "stopped": "Syncing stopped",
"announced": "Announce sent",
- "connected": "Peer connect requested",
- "disconnected": "Peer disconnected",
- "browse_done": "Browse complete",
- "download_started": "Download requested",
- "file_updated": "File updated",
- "file_deleted": "File deleted",
+ "connected": "Connecting to device...",
+ "disconnected": "Device removed",
+ "browse_done": "Remote file list ready",
+ "download_started": "Download started",
+ "file_updated": "A file changed",
+ "file_deleted": "A file was removed",
"copied": "Copied to clipboard",
- "error": "FileSync error"
+ "error": "File Sync error"
}
}

diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index 2ef283cc..54ac9c33 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -2608,8 +2608,8 @@
"description": "IP-туннелирование по каналам Reticulum (в разработке)."
},
"rns_filesync": {
- "title": "RNS FileSync",
- "description": "Sync a directory with mesh peers over Reticulum using destination hashes."
+ "title": "File Sync",
+ "description": "Keep a shared folder in sync with mesh friends, like Syncthing over Reticulum."
},
"bots": {
"title": "LXMFy Боты",
@@ -3257,8 +3257,8 @@
"nav_rnsh_desc": "Удалённая оболочка через Reticulum",
"nav_rnx": "RNX",
"nav_rnx_desc": "Удалённое выполнение команд через Reticulum",
- "nav_rns_filesync": "RNS FileSync",
- "nav_rns_filesync_desc": "Directory sync over Reticulum"
+ "nav_rns_filesync": "File Sync",
+ "nav_rns_filesync_desc": "Shared folders over the mesh"
},
"settings": {
"shortcut_saved": "Ярлык сохранён",
@@ -3821,55 +3821,89 @@
"windows_screen_security_later": "Не сейчас"
},
"rns_filesync": {
- "eyebrow": "Directory sync",
- "title": "RNS FileSync",
- "description": "Sync a local directory with mesh peers over Reticulum. Peers are identity hashes on aspect rns_filesync.filesync.",
- "usage_steps": "How to use FileSync:",
- "step_1": "1. Set a sync directory, start FileSync, and share your destination hash with peers.",
- "step_2": "2. Connect to a peer by identity hash, then browse or download files from the Browse tab.",
- "step_3": "3. Use the ACL tab to grant read, write, or delete permissions when enforcement is enabled.",
- "tab_status": "Status",
- "tab_peers": "Peers",
+ "eyebrow": "Shared folders",
+ "title": "File Sync",
+ "description": "Keep a folder in sync with people on your mesh. Share your sync ID, connect devices, and files move both ways when allowed.",
+ "usage_steps": "Quick start",
+ "step_1": "1. Choose a folder, press Start, then share your sync ID with a friend.",
+ "step_2": "2. On Devices, paste their sync ID to connect. Files appear under Files as they sync.",
+ "step_3": "3. Under Sharing, decide who can read, write, or delete when access control is on.",
+ "tab_folder": "Folder",
+ "tab_devices": "Devices",
"tab_files": "Files",
- "tab_browse": "Browse",
- "tab_acl": "ACL",
- "sync_directory": "Sync directory",
- "sync_directory_placeholder": "Path under this identity storage",
- "announce_interval": "Announce interval (seconds)",
- "monitor": "Watch directory for changes",
- "start": "Start",
- "stop": "Stop",
+ "tab_remote": "Remote",
+ "tab_sharing": "Sharing",
+ "tab_status": "Folder",
+ "tab_peers": "Devices",
+ "tab_browse": "Remote",
+ "tab_acl": "Sharing",
+ "sync_directory": "Folder to sync",
+ "sync_directory_placeholder": "Choose a folder under this identity",
+ "sync_directory_help": "Folders stay inside this identity's MeshChat storage. Use Browse on any device to pick or create one.",
+ "announce_interval": "Announce every (seconds)",
+ "announce_interval_help": "How often others are reminded you are available to sync.",
+ "monitor": "Watch folder for changes",
+ "start": "Start syncing",
+ "stop": "Stop syncing",
"announce": "Announce now",
"refresh": "Refresh",
"running": "Running",
+ "status_syncing": "Syncing",
+ "status_stopped": "Stopped",
"yes": "Yes",
"no": "No",
- "peers_count": "Peers",
+ "peers_count": "Devices",
"files_count": "Files",
- "destination_hash": "Destination hash",
- "last_progress": "Last progress",
- "peer_hash_placeholder": "Identity hash (hex)",
+ "destination_hash": "Sync ID",
+ "share_id": "Your sync ID",
+ "share_id_help": "Send this to friends so they can connect. Tap to copy.",
+ "last_progress": "Latest transfer",
+ "peer_hash_placeholder": "Friend's sync ID",
"connect": "Connect",
- "disconnect": "Disconnect",
- "no_peers": "No connected peers yet.",
- "no_files": "No local files in the sync inventory yet.",
- "select_peer": "Select a peer",
- "browse": "Browse",
- "no_remote_files": "No remote files listed. Connect and browse a peer.",
- "download": "Download",
- "acl_enforce": "Enforce ACL (deny by default)",
- "acl_grant": "Grant",
- "acl_updated": "ACL updated",
- "started": "FileSync started",
- "stopped": "FileSync stopped",
+ "disconnect": "Remove",
+ "no_peers": "No devices connected yet. Start syncing, then paste a friend’s sync ID.",
+ "no_files": "This folder is empty so far. Drop files in, or pull some from Remote.",
+ "select_peer": "Choose a device",
+ "browse": "List files",
+ "no_remote_files": "No remote files yet. Connect a device, then list their files.",
+ "download": "Get file",
+ "acl_enforce": "Only allow listed devices (deny everyone else)",
+ "acl_grant": "Allow device",
+ "acl_updated": "Sharing rules updated",
+ "perm_read": "Read",
+ "perm_write": "Write",
+ "perm_delete": "Delete",
+ "no_acl_rules": "No sharing rules yet. Add a device above when access control is on.",
+ "sharing_help": "Control who may read, change, or delete files in your shared folder.",
+ "devices_help": "Connect to another person’s sync ID. Both sides should be syncing.",
+ "remote_help": "Peek at a connected device’s files and pull one into your folder.",
+ "browse_folder": "Browse",
+ "open_folder": "Open",
+ "browser_title": "Choose sync folder",
+ "browser_hint": "Browse folders for this identity. Works the same on desktop, web, and mobile.",
+ "browser_up": "Up",
+ "browser_loading": "Loading folders...",
+ "browser_empty": "No subfolders here. Create one below, or select this folder.",
+ "browser_new": "New folder",
+ "browser_new_placeholder": "Folder name",
+ "browser_select": "Use this folder",
+ "browser_created": "Folder created",
+ "folder_selected": "Folder selected",
+ "path_copied": "Folder path copied",
+ "stop_before_change_folder": "Stop syncing before changing the folder.",
+ "peer_connected": "Connected",
+ "peer_disconnected": "Disconnected",
+ "peer_unknown": "Unknown",
+ "started": "Syncing started",
+ "stopped": "Syncing stopped",
"announced": "Announce sent",
- "connected": "Peer connect requested",
- "disconnected": "Peer disconnected",
- "browse_done": "Browse complete",
- "download_started": "Download requested",
- "file_updated": "File updated",
- "file_deleted": "File deleted",
+ "connected": "Connecting to device...",
+ "disconnected": "Device removed",
+ "browse_done": "Remote file list ready",
+ "download_started": "Download started",
+ "file_updated": "A file changed",
+ "file_deleted": "A file was removed",
"copied": "Copied to clipboard",
- "error": "FileSync error"
+ "error": "File Sync error"
}
}

diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index b386af96..94a7cb21 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -2804,8 +2804,8 @@
"description": "IP地道通向Reticulum链接(正在开发)."
},
"rns_filesync": {
- "title": "RNS FileSync",
- "description": "Sync a directory with mesh peers over Reticulum using destination hashes."
+ "title": "File Sync",
+ "description": "Keep a shared folder in sync with mesh friends, like Syncthing over Reticulum."
},
"bots": {
"title": "LXMFy 波兹",
@@ -3505,8 +3505,8 @@
"nav_rnsh_desc": "通过 Reticulum 的远程 Shell",
"nav_rnx": "RNX",
"nav_rnx_desc": "通过 Reticulum 远程执行命令",
- "nav_rns_filesync": "RNS FileSync",
- "nav_rns_filesync_desc": "Directory sync over Reticulum"
+ "nav_rns_filesync": "File Sync",
+ "nav_rns_filesync_desc": "Shared folders over the mesh"
},
"relay_chat": {
"title": "中继聊天",
@@ -3821,55 +3821,89 @@
"windows_screen_security_later": "暂不"
},
"rns_filesync": {
- "eyebrow": "Directory sync",
- "title": "RNS FileSync",
- "description": "Sync a local directory with mesh peers over Reticulum. Peers are identity hashes on aspect rns_filesync.filesync.",
- "usage_steps": "How to use FileSync:",
- "step_1": "1. Set a sync directory, start FileSync, and share your destination hash with peers.",
- "step_2": "2. Connect to a peer by identity hash, then browse or download files from the Browse tab.",
- "step_3": "3. Use the ACL tab to grant read, write, or delete permissions when enforcement is enabled.",
- "tab_status": "Status",
- "tab_peers": "Peers",
+ "eyebrow": "Shared folders",
+ "title": "File Sync",
+ "description": "Keep a folder in sync with people on your mesh. Share your sync ID, connect devices, and files move both ways when allowed.",
+ "usage_steps": "Quick start",
+ "step_1": "1. Choose a folder, press Start, then share your sync ID with a friend.",
+ "step_2": "2. On Devices, paste their sync ID to connect. Files appear under Files as they sync.",
+ "step_3": "3. Under Sharing, decide who can read, write, or delete when access control is on.",
+ "tab_folder": "Folder",
+ "tab_devices": "Devices",
"tab_files": "Files",
- "tab_browse": "Browse",
- "tab_acl": "ACL",
- "sync_directory": "Sync directory",
- "sync_directory_placeholder": "Path under this identity storage",
- "announce_interval": "Announce interval (seconds)",
- "monitor": "Watch directory for changes",
- "start": "Start",
- "stop": "Stop",
+ "tab_remote": "Remote",
+ "tab_sharing": "Sharing",
+ "tab_status": "Folder",
+ "tab_peers": "Devices",
+ "tab_browse": "Remote",
+ "tab_acl": "Sharing",
+ "sync_directory": "Folder to sync",
+ "sync_directory_placeholder": "Choose a folder under this identity",
+ "sync_directory_help": "Folders stay inside this identity's MeshChat storage. Use Browse on any device to pick or create one.",
+ "announce_interval": "Announce every (seconds)",
+ "announce_interval_help": "How often others are reminded you are available to sync.",
+ "monitor": "Watch folder for changes",
+ "start": "Start syncing",
+ "stop": "Stop syncing",
"announce": "Announce now",
"refresh": "Refresh",
"running": "Running",
+ "status_syncing": "Syncing",
+ "status_stopped": "Stopped",
"yes": "Yes",
"no": "No",
- "peers_count": "Peers",
+ "peers_count": "Devices",
"files_count": "Files",
- "destination_hash": "Destination hash",
- "last_progress": "Last progress",
- "peer_hash_placeholder": "Identity hash (hex)",
+ "destination_hash": "Sync ID",
+ "share_id": "Your sync ID",
+ "share_id_help": "Send this to friends so they can connect. Tap to copy.",
+ "last_progress": "Latest transfer",
+ "peer_hash_placeholder": "Friend's sync ID",
"connect": "Connect",
- "disconnect": "Disconnect",
- "no_peers": "No connected peers yet.",
- "no_files": "No local files in the sync inventory yet.",
- "select_peer": "Select a peer",
- "browse": "Browse",
- "no_remote_files": "No remote files listed. Connect and browse a peer.",
- "download": "Download",
- "acl_enforce": "Enforce ACL (deny by default)",
- "acl_grant": "Grant",
- "acl_updated": "ACL updated",
- "started": "FileSync started",
- "stopped": "FileSync stopped",
+ "disconnect": "Remove",
+ "no_peers": "No devices connected yet. Start syncing, then paste a friend’s sync ID.",
+ "no_files": "This folder is empty so far. Drop files in, or pull some from Remote.",
+ "select_peer": "Choose a device",
+ "browse": "List files",
+ "no_remote_files": "No remote files yet. Connect a device, then list their files.",
+ "download": "Get file",
+ "acl_enforce": "Only allow listed devices (deny everyone else)",
+ "acl_grant": "Allow device",
+ "acl_updated": "Sharing rules updated",
+ "perm_read": "Read",
+ "perm_write": "Write",
+ "perm_delete": "Delete",
+ "no_acl_rules": "No sharing rules yet. Add a device above when access control is on.",
+ "sharing_help": "Control who may read, change, or delete files in your shared folder.",
+ "devices_help": "Connect to another person’s sync ID. Both sides should be syncing.",
+ "remote_help": "Peek at a connected device’s files and pull one into your folder.",
+ "browse_folder": "Browse",
+ "open_folder": "Open",
+ "browser_title": "Choose sync folder",
+ "browser_hint": "Browse folders for this identity. Works the same on desktop, web, and mobile.",
+ "browser_up": "Up",
+ "browser_loading": "Loading folders...",
+ "browser_empty": "No subfolders here. Create one below, or select this folder.",
+ "browser_new": "New folder",
+ "browser_new_placeholder": "Folder name",
+ "browser_select": "Use this folder",
+ "browser_created": "Folder created",
+ "folder_selected": "Folder selected",
+ "path_copied": "Folder path copied",
+ "stop_before_change_folder": "Stop syncing before changing the folder.",
+ "peer_connected": "Connected",
+ "peer_disconnected": "Disconnected",
+ "peer_unknown": "Unknown",
+ "started": "Syncing started",
+ "stopped": "Syncing stopped",
"announced": "Announce sent",
- "connected": "Peer connect requested",
- "disconnected": "Peer disconnected",
- "browse_done": "Browse complete",
- "download_started": "Download requested",
- "file_updated": "File updated",
- "file_deleted": "File deleted",
+ "connected": "Connecting to device...",
+ "disconnected": "Device removed",
+ "browse_done": "Remote file list ready",
+ "download_started": "Download started",
+ "file_updated": "A file changed",
+ "file_deleted": "A file was removed",
"copied": "Copied to clipboard",
- "error": "FileSync error"
+ "error": "File Sync error"
}
}

diff --git a/tests/backend/fixtures/http_api_routes.json b/tests/backend/fixtures/http_api_routes.json
index be57e21e..83ef20e7 100644
--- a/tests/backend/fixtures/http_api_routes.json
+++ b/tests/backend/fixtures/http_api_routes.json
@@ -320,6 +320,14 @@
"method": "POST",
"path": "/api/v1/filesync/connect"
},
+ {
+ "method": "GET",
+ "path": "/api/v1/filesync/directories"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/filesync/directories"
+ },
{
"method": "POST",
"path": "/api/v1/filesync/disconnect"
@@ -528,6 +536,10 @@
"method": "POST",
"path": "/api/v1/lxmf/message-blocklist/import"
},
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/propagation-node/cancel-inbound"
+ },
{
"method": "POST",
"path": "/api/v1/lxmf/propagation-node/restart"
@@ -1200,6 +1212,10 @@
"method": "POST",
"path": "/api/v1/rrc/hubs/{hub_hash}/disconnect"
},
+ {
+ "method": "GET",
+ "path": "/api/v1/rrc/hubs/{hub_hash}/room-keys"
+ },
{
"method": "POST",
"path": "/api/v1/rrc/hubs/{hub_hash}/rooms"
@@ -1212,6 +1228,14 @@
"method": "DELETE",
"path": "/api/v1/rrc/hubs/{hub_hash}/rooms/{room}"
},
+ {
+ "method": "DELETE",
+ "path": "/api/v1/rrc/hubs/{hub_hash}/rooms/{room}/key"
+ },
+ {
+ "method": "PUT",
+ "path": "/api/v1/rrc/hubs/{hub_hash}/rooms/{room}/key"
+ },
{
"method": "DELETE",
"path": "/api/v1/rrc/hubs/{hub_hash}/rooms/{room}/messages"
@@ -1272,6 +1296,10 @@
"method": "DELETE",
"path": "/api/v1/rrc/servers/{hub_id}/rooms/{room}"
},
+ {
+ "method": "PUT",
+ "path": "/api/v1/rrc/servers/{hub_id}/rooms/{room}/key"
+ },
{
"method": "POST",
"path": "/api/v1/rrc/servers/{hub_id}/start"

diff --git a/tests/backend/http_api_response_registry.py b/tests/backend/http_api_response_registry.py
index 22e70604..11157f8c 100644
--- a/tests/backend/http_api_response_registry.py
+++ b/tests/backend/http_api_response_registry.py
@@ -85,6 +85,7 @@ from tests.backend.http_api_response_schemas import (
RNCP_STATUS_SCHEMA,
RNCP_TRANSFER_SCHEMA,
FILESYNC_ACL_SCHEMA,
+ FILESYNC_DIRECTORIES_SCHEMA,
FILESYNC_FILES_SCHEMA,
FILESYNC_PEERS_SCHEMA,
FILESYNC_STATUS_SCHEMA,
@@ -453,10 +454,41 @@ HTTP_JSON_GET_CONTRACTS: tuple[HttpJsonContract, ...] = (
allow_statuses=(200, 404),
alt_schemas=(MESSAGE_ENVELOPE_SCHEMA,),
),
- HttpJsonContract("GET", "/api/v1/filesync/status", FILESYNC_STATUS_SCHEMA),
- HttpJsonContract("GET", "/api/v1/filesync/peers", FILESYNC_PEERS_SCHEMA),
- HttpJsonContract("GET", "/api/v1/filesync/files", FILESYNC_FILES_SCHEMA),
- HttpJsonContract("GET", "/api/v1/filesync/acl", FILESYNC_ACL_SCHEMA),
+ HttpJsonContract(
+ "GET",
+ "/api/v1/filesync/status",
+ FILESYNC_STATUS_SCHEMA,
+ allow_statuses=(200, 503),
+ alt_schemas=(MESSAGE_ENVELOPE_SCHEMA,),
+ ),
+ HttpJsonContract(
+ "GET",
+ "/api/v1/filesync/peers",
+ FILESYNC_PEERS_SCHEMA,
+ allow_statuses=(200, 503),
+ alt_schemas=(MESSAGE_ENVELOPE_SCHEMA,),
+ ),
+ HttpJsonContract(
+ "GET",
+ "/api/v1/filesync/files",
+ FILESYNC_FILES_SCHEMA,
+ allow_statuses=(200, 503),
+ alt_schemas=(MESSAGE_ENVELOPE_SCHEMA,),
+ ),
+ HttpJsonContract(
+ "GET",
+ "/api/v1/filesync/directories",
+ FILESYNC_DIRECTORIES_SCHEMA,
+ allow_statuses=(200, 400, 503),
+ alt_schemas=(MESSAGE_ENVELOPE_SCHEMA,),
+ ),
+ HttpJsonContract(
+ "GET",
+ "/api/v1/filesync/acl",
+ FILESYNC_ACL_SCHEMA,
+ allow_statuses=(200, 503),
+ alt_schemas=(MESSAGE_ENVELOPE_SCHEMA,),
+ ),
HttpJsonContract("GET", "/api/v1/rnpath/table", RNPATH_TABLE_SCHEMA),
HttpJsonContract("GET", "/api/v1/rnpath/rates", RNPATH_RATES_SCHEMA),
HttpJsonContract(

diff --git a/tests/backend/http_api_response_schemas.py b/tests/backend/http_api_response_schemas.py
index 80a2efff..83fc5f2e 100644
--- a/tests/backend/http_api_response_schemas.py
+++ b/tests/backend/http_api_response_schemas.py
@@ -542,6 +542,7 @@ FILESYNC_STATUS_SCHEMA: dict = {
"monitor": _BOOLEAN,
"announce_interval": _INTEGER,
"config_directory": _STRING,
+ "storage_directory": _STRING,
},
"additionalProperties": True,
}
@@ -560,6 +561,19 @@ FILESYNC_FILES_SCHEMA: dict = {
"additionalProperties": True,
}
+FILESYNC_DIRECTORIES_SCHEMA: dict = {
+ "type": "object",
+ "required": ["ok", "root", "current", "directories"],
+ "properties": {
+ "ok": _BOOLEAN,
+ "root": _STRING,
+ "current": _STRING,
+ "parent": {"type": ["string", "null"]},
+ "directories": _ARRAY,
+ },
+ "additionalProperties": True,
+}
+
FILESYNC_ACL_SCHEMA: dict = {
"type": "object",
"required": ["enforce", "rules"],

diff --git a/tests/backend/test_plugin_signature.py b/tests/backend/test_plugin_signature.py
index f2be1d96..e3881538 100644
--- a/tests/backend/test_plugin_signature.py
+++ b/tests/backend/test_plugin_signature.py
@@ -46,6 +46,21 @@ def test_canonical_dir_zip_roundtrip(tmp_path):
assert PRIMARY_SIGNATURE_FILE not in names
+def test_canonical_dir_ignores_pycache_bytecode(tmp_path):
+ plugin_dir = tmp_path / "plugin"
+ _write_plugin_dir(plugin_dir)
+ before = canonical_dir_payload(str(plugin_dir))
+ cache = plugin_dir / "backend" / "__pycache__"
+ cache.mkdir(parents=True)
+ (cache / "main.cpython-314.pyc").write_bytes(b"bytecode")
+ (plugin_dir / "frontend" / "main.pyc").write_bytes(b"also-bytecode")
+ after = canonical_dir_payload(str(plugin_dir))
+ assert before == after
+ with zipfile.ZipFile(io.BytesIO(after)) as archive:
+ names = archive.namelist()
+ assert not any("__pycache__" in name or name.endswith(".pyc") for name in names)
+
+
def test_unsigned_ok_invalid_blocks(tmp_path):
plugin_dir = tmp_path / "plugin"
_write_plugin_dir(plugin_dir)

diff --git a/tests/backend/test_rns_filesync_handler.py b/tests/backend/test_rns_filesync_handler.py
index f5310004..d3e78e73 100644
--- a/tests/backend/test_rns_filesync_handler.py
+++ b/tests/backend/test_rns_filesync_handler.py
@@ -33,6 +33,39 @@ def test_default_status_not_running(handler, mock_identity):
assert status["identity_hash"] == mock_identity.hash.hex()
assert status["sync_directory"].endswith("filesync/sync")
assert "filesync" in status["config_directory"]
+ assert status["storage_directory"] == handler.storage_dir
+
+
+def test_list_directories_defaults_to_filesync_root(handler):
+ nested = f"{handler.storage_dir}/filesync/photos"
+ import os
+
+ os.makedirs(nested, exist_ok=True)
+ result = handler.list_directories()
+ assert result["ok"] is True
+ assert result["current"].endswith("filesync")
+ names = {entry["name"] for entry in result["directories"]}
+ assert "photos" in names or "sync" in names
+
+
+def test_list_directories_rejects_outside_jail(handler):
+ result = handler.list_directories("/tmp/outside")
+ assert result["ok"] is False
+ assert "identity storage" in result["error"]
+
+
+def test_create_directory_under_filesync(handler):
+ result = handler.create_directory(None, "shared")
+ assert result["ok"] is True
+ assert result["path"].endswith("filesync/shared")
+ listed = handler.list_directories(handler._root)
+ assert any(entry["name"] == "shared" for entry in listed["directories"])
+
+
+def test_create_directory_rejects_traversal_name(handler):
+ result = handler.create_directory(handler._root, "../escape")
+ assert result["ok"] is False
+ assert "invalid" in result["error"]
def test_update_settings_persists_sync_directory(handler):

diff --git a/tests/backend/test_rns_filesync_security.py b/tests/backend/test_rns_filesync_security.py
index 6d43e16d..8de6d4f8 100644
--- a/tests/backend/test_rns_filesync_security.py
+++ b/tests/backend/test_rns_filesync_security.py
@@ -90,6 +90,22 @@ def test_download_and_browse_require_running(handler):
assert handler.connect_peer("bb" * 16)["ok"] is False
+def test_list_directories_rejects_traversal(handler):
+ for payload in ("../etc", "/etc/passwd", handler.storage_dir + "/../"):
+ result = handler.list_directories(payload)
+ # parent of storage may resolve outside and fail jail
+ if result.get("ok"):
+ assert result["current"].startswith(handler.storage_dir)
+ else:
+ assert "identity storage" in result["error"] or "not a directory" in result["error"]
+
+
+def test_create_directory_rejects_dotfiles_and_separators(handler):
+ assert handler.create_directory(handler._root, ".hidden")["ok"] is False
+ assert handler.create_directory(handler._root, "a/b")["ok"] is False
+ assert handler.create_directory("/etc", "nope")["ok"] is False
+
+
@pytest.mark.parametrize("bad_hash", _BAD_HASHES)
def test_connect_rejects_bad_hashes(handler, bad_hash):
handler.service = MagicMock()

diff --git a/tests/backend/test_rns_link_plugin.py b/tests/backend/test_rns_link_plugin.py
index c6dea0d6..f131697f 100644
--- a/tests/backend/test_rns_link_plugin.py
+++ b/tests/backend/test_rns_link_plugin.py
@@ -287,7 +287,10 @@ class TestRnsLinkPluginCapabilities:
deadline=None,
suppress_health_check=[HealthCheck.function_scoped_fixture],
)
- def test_open_close_property(self, tmp_path, dest, aspect):
+ def test_open_close_property(self, tmp_path_factory, dest, aspect):
+ # Fresh plugin storage per Hypothesis example avoids integrity flakes
+ # when a prior example activated Python bytecode under the install tree.
+ tmp_path = tmp_path_factory.mktemp("rns_link")
fake = FakeLinkManager()
class FakeApp:

diff --git a/tests/backend/test_rrc_oracle_bugs.py b/tests/backend/test_rrc_oracle_bugs.py
index 2874e693..d3e9599e 100644
--- a/tests/backend/test_rrc_oracle_bugs.py
+++ b/tests/backend/test_rrc_oracle_bugs.py
@@ -238,6 +238,30 @@ def test_oracle_register_room_key_strips_like_join_paths():
)
+def test_oracle_topic_private_hides_from_outsiders_and_blocks_set():
+ server = make_server()
+ link_a, sess_a = add_session(server, b"\xaa" * 16, nick="alice")
+ link_spy, sess_spy = add_session(server, b"\xbb" * 16, nick="spy")
+ server.register_room("secret", private=True, founder=sess_a.peer, topic="hidden")
+ join(server, link_a, sess_a, "secret")
+
+ read = msg(server, link_spy, sess_spy, "lobby", "/topic secret")
+ notices = envs_of_type(read, proto.T_NOTICE, to_link=link_spy)
+ assert notices
+ assert notices[0][proto.K_BODY] == "topic for secret: (none)"
+
+ vandal = msg(server, link_spy, sess_spy, "lobby", "/topic secret owned")
+ assert envs_of_type(vandal, proto.T_ERROR, to_link=link_spy)
+ assert server.rooms.get_state("secret")["topic"] == "hidden"
+
+
+def test_oracle_topic_read_does_not_create_ghost_state():
+ server = make_server()
+ link, sess = add_session(server, b"\xaa" * 16, nick="alice")
+ msg(server, link, sess, "lobby", "/topic ghostroom")
+ assert server.rooms.get_state("ghostroom") is None
+
+
def test_oracle_who_on_private_room_requires_membership():
server = make_server()
link_a, sess_a = add_session(server, b"\xaa" * 16, nick="alice")

diff --git a/tests/backend/test_rrc_room_keys.py b/tests/backend/test_rrc_room_keys.py
index 83578ea4..a1756d6d 100644
--- a/tests/backend/test_rrc_room_keys.py
+++ b/tests/backend/test_rrc_room_keys.py
@@ -169,6 +169,15 @@ def test_bad_key_error_detector():
assert RRCManager.is_bad_key_error(None) is False
+def test_redact_mode_plus_k_for_history():
+ assert (
+ RRCHub._redact_command_for_history("/mode vault +k supersecret")
+ == "/mode vault +k ***"
+ )
+ assert RRCHub._redact_command_for_history("/who lobby") == "/who lobby"
+ assert RRCHub._redact_command_for_history("/mode vault -k") == "/mode vault -k"
+
+
def test_handle_error_forgets_bad_key(db, tmp_path):
identity = _Identity(b"\x11" * 16, PRIVATE_A)
manager = RRCManager(identity, str(tmp_path), database=db)

diff --git a/tests/frontend/ConfirmDialog.test.js b/tests/frontend/ConfirmDialog.test.js
index 9e580288..958aa796 100644
--- a/tests/frontend/ConfirmDialog.test.js
+++ b/tests/frontend/ConfirmDialog.test.js
@@ -75,4 +75,18 @@ describe("ConfirmDialog UI", () => {
expect(resolve).toHaveBeenCalledWith(false);
expect(wrapper.vm.pendingConfirm).toBeNull();
});
+
+ it("resolves the previous waiter with false when a second confirm opens", async () => {
+ const first = vi.fn();
+ const second = vi.fn();
+ const wrapper = mountDialog();
+ const showFn = GlobalEmitter.on.mock.calls.find((c) => c[0] === "confirm")?.[1];
+ showFn({ message: "First?", resolve: first });
+ await wrapper.vm.$nextTick();
+ showFn({ message: "Second?", resolve: second });
+ await wrapper.vm.$nextTick();
+ expect(first).toHaveBeenCalledWith(false);
+ expect(wrapper.vm.pendingConfirm).toEqual({ message: "Second?" });
+ expect(second).not.toHaveBeenCalled();
+ });
});

diff --git a/tests/frontend/NomadNetworkPage.test.js b/tests/frontend/NomadNetworkPage.test.js
index 5258ab66..4cf68e3b 100644
--- a/tests/frontend/NomadNetworkPage.test.js
+++ b/tests/frontend/NomadNetworkPage.test.js
@@ -381,6 +381,13 @@ describe("NomadNetworkPage.vue", () => {
vi.useRealTimers();
});
+ it("relative page URL without selected node toasts instead of crashing", async () => {
+ const wrapper = mountNomadNetworkPage();
+ wrapper.vm.selectedNode = null;
+ await expect(wrapper.vm.onNodePageUrlClick(":/page/index.mu")).resolves.toBeUndefined();
+ expect(ToastUtils.warning).toHaveBeenCalled();
+ });
+
it("does not re-run Micron conversion when only favourites list updates", async () => {
const dest = "b".repeat(32);
const wrapper = mountNomadNetworkPage();

diff --git a/tests/frontend/PromptDialog.test.js b/tests/frontend/PromptDialog.test.js
index a38501be..b1f51fc1 100644
--- a/tests/frontend/PromptDialog.test.js
+++ b/tests/frontend/PromptDialog.test.js
@@ -66,4 +66,18 @@ describe("PromptDialog UI", () => {
expect(resolve).toHaveBeenCalledWith(null);
expect(wrapper.vm.pendingPrompt).toBeNull();
});
+
+ it("resolves the previous waiter with null when a second prompt opens", async () => {
+ const first = vi.fn();
+ const second = vi.fn();
+ const wrapper = mountDialog();
+ const showFn = GlobalEmitter.on.mock.calls.find((c) => c[0] === "prompt")?.[1];
+ showFn({ message: "First?", defaultValue: "a", resolve: first });
+ await wrapper.vm.$nextTick();
+ showFn({ message: "Second?", defaultValue: "b", resolve: second });
+ await wrapper.vm.$nextTick();
+ expect(first).toHaveBeenCalledWith(null);
+ expect(wrapper.vm.pendingPrompt).toEqual({ message: "Second?" });
+ expect(second).not.toHaveBeenCalled();
+ });
});

diff --git a/tests/frontend/RelayChatPage.test.js b/tests/frontend/RelayChatPage.test.js
index 40297b98..ad8172a8 100644
--- a/tests/frontend/RelayChatPage.test.js
+++ b/tests/frontend/RelayChatPage.test.js
@@ -709,4 +709,32 @@ describe("RelayChatPage.vue", () => {
await vi.waitFor(() => expect(axiosMock.get).toHaveBeenCalledWith("/api/v1/rrc/servers"));
});
+
+ it("isBadKeyErrorText requires bad key wording not bare +k", () => {
+ const wrapper = mountPage();
+ expect(wrapper.vm.isBadKeyErrorText("bad key (+k)")).toBe(true);
+ expect(wrapper.vm.isBadKeyErrorText("ERROR: Bad Key (+K)")).toBe(true);
+ expect(wrapper.vm.isBadKeyErrorText("failed: enable +k first")).toBe(false);
+ expect(wrapper.vm.isBadKeyErrorText("+k")).toBe(false);
+ });
+
+ it("sendModerationCommand prefers peer hash over display nick", async () => {
+ const wrapper = mountPage();
+ await vi.waitFor(() => expect(wrapper.vm.hubs.length).toBeGreaterThan(0));
+ wrapper.vm.selectedHubHash = HUB_HASH;
+ wrapper.vm.selectedRoom = "lobby";
+ axiosMock.post.mockResolvedValueOnce({ data: {} });
+ await wrapper.vm.sendModerationCommand("/kick {room} {target}", {
+ src: "aabbccddeeff00112233445566778899",
+ nick: "Alice With Spaces",
+ text: "hi",
+ });
+ expect(axiosMock.post).toHaveBeenCalledWith(
+ `/api/v1/rrc/hubs/${HUB_HASH}/command`,
+ expect.objectContaining({
+ text: "/kick lobby aabbccddeeff00112233445566778899",
+ room: "lobby",
+ })
+ );
+ });
});

diff --git a/tests/frontend/RnsFilesyncPage.test.js b/tests/frontend/RnsFilesyncPage.test.js
index 2e1efd8b..434ddded 100644
--- a/tests/frontend/RnsFilesyncPage.test.js
+++ b/tests/frontend/RnsFilesyncPage.test.js
@@ -17,6 +17,14 @@ vi.mock("@/js/registries/wsEventRegistry.js", () => ({
offWsEvent: vi.fn(),
}));
+vi.mock("@/js/ElectronUtils", () => ({
+ default: {
+ openDirectoryOrCopy: vi.fn().mockResolvedValue(true),
+ isElectron: vi.fn().mockReturnValue(false),
+ pickDirectory: vi.fn().mockResolvedValue(null),
+ },
+}));
+
describe("RnsFilesyncPage.vue", () => {
let apiMock;
@@ -34,6 +42,7 @@ describe("RnsFilesyncPage.vue", () => {
data: {
running: false,
sync_directory: "/tmp/sync",
+ storage_directory: "/tmp",
peers: 0,
files: 0,
monitor: true,
@@ -50,6 +59,17 @@ describe("RnsFilesyncPage.vue", () => {
if (url === "/api/v1/filesync/acl") {
return Promise.resolve({ data: { enforce: false, rules: {} } });
}
+ if (String(url).startsWith("/api/v1/filesync/directories")) {
+ return Promise.resolve({
+ data: {
+ ok: true,
+ root: "/tmp",
+ current: "/tmp/sync",
+ parent: "/tmp",
+ directories: [{ name: "docs", path: "/tmp/sync/docs" }],
+ },
+ });
+ }
return Promise.resolve({ data: {} });
});
apiMock.post.mockResolvedValue({ data: { ok: true } });
@@ -76,6 +96,11 @@ describe("RnsFilesyncPage.vue", () => {
template: "<div class='header-stub'>{{ title }}</div>",
props: ["title", "description", "eyebrow", "icon", "accent"],
},
+ FilesyncDirectoryBrowserModal: {
+ template: "<div class='browser-stub' v-if='open'></div>",
+ props: ["open", "initialPath"],
+ emits: ["close", "select"],
+ },
},
},
});
@@ -87,6 +112,33 @@ describe("RnsFilesyncPage.vue", () => {
expect(wrapper.vm.syncDirectory).toBe("/tmp/sync");
});
+ it("uses themed input-field classes", async () => {
+ const wrapper = mountPage();
+ await vi.waitFor(() => expect(wrapper.vm.syncDirectory).toBe("/tmp/sync"));
+ expect(wrapper.find("input.input-field").exists()).toBe(true);
+ expect(wrapper.find("input.glass-input").exists()).toBe(false);
+ });
+
+ it("opens directory browser from browse button", async () => {
+ const wrapper = mountPage();
+ await vi.waitFor(() => expect(wrapper.vm.syncDirectory).toBe("/tmp/sync"));
+ expect(wrapper.vm.directoryBrowserOpen).toBe(false);
+ await wrapper.vm.openDirectoryBrowser();
+ expect(wrapper.vm.directoryBrowserOpen).toBe(true);
+ wrapper.vm.onDirectorySelected("/tmp/sync/docs");
+ expect(wrapper.vm.syncDirectory).toBe("/tmp/sync/docs");
+ expect(ToastUtils.success).toHaveBeenCalledWith("rns_filesync.folder_selected");
+ });
+
+ it("warns when browsing while syncing", async () => {
+ const wrapper = mountPage();
+ await vi.waitFor(() => expect(wrapper.vm.syncDirectory).toBe("/tmp/sync"));
+ wrapper.vm.status.running = true;
+ await wrapper.vm.openDirectoryBrowser();
+ expect(wrapper.vm.directoryBrowserOpen).toBe(false);
+ expect(ToastUtils.warning).toHaveBeenCalledWith("rns_filesync.stop_before_change_folder");
+ });
+
it("starts filesync and toasts success", async () => {
const wrapper = mountPage();
await vi.waitFor(() => expect(wrapper.vm.syncDirectory).toBe("/tmp/sync"));
@@ -133,4 +185,80 @@ describe("RnsFilesyncPage.vue", () => {
});
expect(ToastUtils.info).toHaveBeenCalledWith("rns_filesync.download_started");
});
+
+ it("builds friendly ACL rows", async () => {
+ const wrapper = mountPage();
+ await vi.waitFor(() => expect(wrapper.vm.syncDirectory).toBe("/tmp/sync"));
+ wrapper.vm.aclRules = {
+ read: ["aa".repeat(16)],
+ write: ["aa".repeat(16)],
+ delete: [],
+ };
+ expect(wrapper.vm.aclRows).toHaveLength(1);
+ expect(wrapper.vm.aclRows[0].permsLabel).toContain("rns_filesync.perm_read");
+ expect(wrapper.vm.aclRows[0].permsLabel).toContain("rns_filesync.perm_write");
+ });
+
+ it("humanizes progress payloads", async () => {
+ const wrapper = mountPage();
+ await vi.waitFor(() => expect(wrapper.vm.syncDirectory).toBe("/tmp/sync"));
+ wrapper.vm.lastProgress = { path: "a.txt", status: "sending", bytes: 10, total: 100 };
+ expect(wrapper.vm.lastProgressLabel).toContain("a.txt");
+ expect(wrapper.vm.lastProgressLabel).toContain("sending");
+ });
+});
+
+describe("FilesyncDirectoryBrowserModal.vue", () => {
+ let apiMock;
+
+ beforeEach(async () => {
+ apiMock = {
+ get: vi.fn().mockResolvedValue({
+ data: {
+ ok: true,
+ root: "/tmp",
+ current: "/tmp/filesync",
+ parent: "/tmp",
+ directories: [{ name: "sync", path: "/tmp/filesync/sync" }],
+ },
+ }),
+ post: vi.fn().mockResolvedValue({ data: { ok: true, path: "/tmp/filesync/new" } }),
+ };
+ window.api = apiMock;
+ });
+
+ afterEach(() => {
+ delete window.api;
+ vi.clearAllMocks();
+ });
+
+ it("loads directories when opened and selects a path", async () => {
+ const { default: FilesyncDirectoryBrowserModal } = await import(
+ "@/components/filesync/FilesyncDirectoryBrowserModal.vue"
+ );
+ const wrapper = mount(FilesyncDirectoryBrowserModal, {
+ props: {
+ open: true,
+ initialPath: "/tmp/filesync/sync",
+ },
+ global: {
+ mocks: { $t: (key) => key },
+ stubs: {
+ MaterialDesignIcon: {
+ template: "<div></div>",
+ props: ["iconName"],
+ },
+ },
+ },
+ });
+ await vi.waitFor(() =>
+ expect(apiMock.get).toHaveBeenCalledWith(
+ expect.stringContaining("/api/v1/filesync/directories")
+ )
+ );
+ expect(wrapper.vm.directories).toHaveLength(1);
+ await wrapper.vm.confirmSelection();
+ expect(wrapper.emitted("select")[0]).toEqual(["/tmp/filesync"]);
+ expect(wrapper.emitted("close")).toBeTruthy();
+ });
});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────